diff --git a/bolt/config.go b/bolt/config.go index 6dcd0378..eab21f1b 100644 --- a/bolt/config.go +++ b/bolt/config.go @@ -2,6 +2,7 @@ package bolt import ( "github.com/asdine/storm" + fm "github.com/hacdias/filemanager" ) type ConfigStore struct { @@ -9,7 +10,12 @@ type ConfigStore struct { } func (c ConfigStore) Get(name string, to interface{}) error { - return c.DB.Get("config", name, to) + err := c.DB.Get("config", name, to) + if err == storm.ErrNotFound { + return fm.ErrNotExist + } + + return err } func (c ConfigStore) Save(name string, from interface{}) error { diff --git a/bolt/share.go b/bolt/share.go index d66c08bb..32fd92c2 100644 --- a/bolt/share.go +++ b/bolt/share.go @@ -12,16 +12,24 @@ type ShareStore struct { func (s ShareStore) Get(hash string) (*fm.ShareLink, error) { var v *fm.ShareLink err := s.DB.One("Hash", hash, &v) + if err == storm.ErrNotFound { + return v, fm.ErrNotExist + } + return v, err } func (s ShareStore) GetByPath(hash string) ([]*fm.ShareLink, error) { var v []*fm.ShareLink err := s.DB.Find("Path", hash, &v) + if err == storm.ErrNotFound { + return v, fm.ErrNotExist + } + return v, err } -func (s ShareStore) Gets(hash string) ([]*fm.ShareLink, error) { +func (s ShareStore) Gets() ([]*fm.ShareLink, error) { var v []*fm.ShareLink err := s.DB.All(&v) return v, err diff --git a/bolt/users.go b/bolt/users.go index 880abfa1..b92ce1a4 100644 --- a/bolt/users.go +++ b/bolt/users.go @@ -15,7 +15,7 @@ func (u UsersStore) Get(id int) (*fm.User, error) { var us *fm.User err := u.DB.One("ID", id, us) if err == storm.ErrNotFound { - return nil, fm.ErrUserNotExist + return nil, fm.ErrNotExist } if err != nil { diff --git a/cmd/filemanager/main.go b/cmd/filemanager/main.go index 684fe53c..72b06536 100644 --- a/cmd/filemanager/main.go +++ b/cmd/filemanager/main.go @@ -10,9 +10,14 @@ import ( "path/filepath" "strings" + "github.com/asdine/storm" + lumberjack "gopkg.in/natefinch/lumberjack.v2" "github.com/hacdias/filemanager" + "github.com/hacdias/filemanager/bolt" + h "github.com/hacdias/filemanager/http" + "github.com/hacdias/filemanager/staticgen" "github.com/hacdias/fileutils" flag "github.com/spf13/pflag" "github.com/spf13/viper" @@ -25,7 +30,7 @@ var ( scope string commands string logfile string - staticgen string + staticg string locale string port int noAuth bool @@ -51,7 +56,7 @@ func init() { flag.BoolVar(&allowNew, "allow-new", true, "Default allow new option for new users") flag.BoolVar(&noAuth, "no-auth", false, "Disables authentication") flag.StringVar(&locale, "locale", "en", "Default locale for new users") - flag.StringVar(&staticgen, "staticgen", "", "Static Generator you want to enable") + flag.StringVar(&staticg, "staticgen", "", "Static Generator you want to enable") flag.BoolVarP(&showVer, "version", "v", false, "Show version") } @@ -148,52 +153,6 @@ func main() { }) } - // Create a File Manager instance. - fm, err := filemanager.New(viper.GetString("Database"), filemanager.User{ - AllowCommands: viper.GetBool("AllowCommands"), - AllowEdit: viper.GetBool("AllowEdit"), - AllowNew: viper.GetBool("AllowNew"), - AllowPublish: viper.GetBool("AllowPublish"), - Commands: viper.GetStringSlice("Commands"), - Rules: []*filemanager.Rule{}, - Locale: viper.GetString("Locale"), - CSS: "", - FileSystem: fileutils.Dir(viper.GetString("Scope")), - }) - - if viper.GetBool("NoAuth") { - fm.NoAuth = true - } - - if err != nil { - log.Fatal(err) - } - - switch viper.GetString("StaticGen") { - case "hugo": - hugo := &filemanager.Hugo{ - Root: viper.GetString("Scope"), - Public: filepath.Join(viper.GetString("Scope"), "public"), - Args: []string{}, - CleanPublic: true, - } - - if err = fm.EnableStaticGen(hugo); err != nil { - log.Fatal(err) - } - case "jekyll": - jekyll := &filemanager.Jekyll{ - Root: viper.GetString("Scope"), - Public: filepath.Join(viper.GetString("Scope"), "_site"), - Args: []string{"build"}, - CleanPublic: true, - } - - if err = fm.EnableStaticGen(jekyll); err != nil { - log.Fatal(err) - } - } - // Builds the address and a listener. laddr := viper.GetString("Address") + ":" + viper.GetString("Port") listener, err := net.Listen("tcp", laddr) @@ -205,7 +164,68 @@ func main() { fmt.Println("Listening on", listener.Addr().String()) // Starts the server. - if err := http.Serve(listener, fm); err != nil { + if err := http.Serve(listener, handler()); err != nil { log.Fatal(err) } } + +func handler() http.Handler { + db, err := storm.Open(viper.GetString("Database")) + if err != nil { + log.Fatal(err) + } + + fm := &filemanager.FileManager{ + NoAuth: viper.GetBool("NoAuth"), + BaseURL: "", + PrefixURL: "", + DefaultUser: &filemanager.User{ + AllowCommands: viper.GetBool("AllowCommands"), + AllowEdit: viper.GetBool("AllowEdit"), + AllowNew: viper.GetBool("AllowNew"), + AllowPublish: viper.GetBool("AllowPublish"), + Commands: viper.GetStringSlice("Commands"), + Rules: []*filemanager.Rule{}, + Locale: viper.GetString("Locale"), + CSS: "", + FileSystem: fileutils.Dir(viper.GetString("Scope")), + }, + Store: &filemanager.Store{ + Config: bolt.ConfigStore{DB: db}, + Users: bolt.UsersStore{DB: db}, + Share: bolt.ShareStore{DB: db}, + }, + } + + err = fm.Load() + if err != nil { + log.Fatal(err) + } + + switch viper.GetString("StaticGen") { + case "hugo": + hugo := &staticgen.Hugo{ + Root: viper.GetString("Scope"), + Public: filepath.Join(viper.GetString("Scope"), "public"), + Args: []string{}, + CleanPublic: true, + } + + if err = fm.Attach(hugo); err != nil { + log.Fatal(err) + } + case "jekyll": + jekyll := &staticgen.Jekyll{ + Root: viper.GetString("Scope"), + Public: filepath.Join(viper.GetString("Scope"), "_site"), + Args: []string{"build"}, + CleanPublic: true, + } + + if err = fm.Attach(jekyll); err != nil { + log.Fatal(err) + } + } + + return h.ServeHTTP(fm) +} diff --git a/file.go b/file.go index 367cec66..8d87cfa5 100644 --- a/file.go +++ b/file.go @@ -7,7 +7,6 @@ import ( "crypto/sha256" "crypto/sha512" "encoding/hex" - "errors" "hash" "io" "io/ioutil" @@ -23,10 +22,6 @@ import ( "github.com/gohugoio/hugo/parser" ) -var ( - ErrInvalidOption = errors.New("Invalid option") -) - // File contains the information about a particular file or directory. type File struct { // Indicates the Kind of view on the front-end (Listing, editor or preview). diff --git a/filemanager.go b/filemanager.go index 928a6922..81c6be42 100644 --- a/filemanager.go +++ b/filemanager.go @@ -54,21 +54,38 @@ package filemanager import ( + "crypto/rand" "errors" "log" "net/http" "os" "os/exec" "reflect" + "regexp" "strings" "time" + "golang.org/x/crypto/bcrypt" + rice "github.com/GeertJohan/go.rice" "github.com/asdine/storm" + "github.com/hacdias/fileutils" "github.com/mholt/caddy" "github.com/robfig/cron" ) +var ( + ErrExist = errors.New("the resource already exists") + ErrNotExist = errors.New("the resource does not exist") + ErrEmptyRequest = errors.New("request body is empty") + ErrEmptyPassword = errors.New("password is empty") + ErrEmptyUsername = errors.New("username is empty") + ErrEmptyScope = errors.New("scope is empty") + ErrWrongDataType = errors.New("wrong data type") + ErrInvalidUpdateField = errors.New("invalid field to update") + ErrInvalidOption = errors.New("Invalid option") +) + // FileManager is a file manager instance. It should be creating using the // 'New' function and not directly. type FileManager struct { @@ -110,6 +127,7 @@ type FileManager struct { // Command is a command function. type Command func(r *http.Request, m *FileManager, u *User) error +// Load loads the configuration from the database. func (m *FileManager) Load() error { // Creates a new File Manager instance with the Users // map and Assets box. @@ -215,30 +233,6 @@ func (m *FileManager) SetBaseURL(url string) { m.BaseURL = strings.TrimSuffix(url, "/") } -// ServeHTTP handles the request. -func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) { - /* code, err := serveHTTP(&RequestContext{ - FileManager: m, - User: nil, - File: nil, - }, w, r) - - if code >= 400 { - w.WriteHeader(code) - - if err == nil { - txt := http.StatusText(code) - log.Printf("%v: %v %v\n", r.URL.Path, code, txt) - w.Write([]byte(txt)) - } - } - - if err != nil { - log.Print(err) - w.Write([]byte(err.Error())) - } */ -} - // Attach attaches a static generator to the current File Manager. func (m *FileManager) Attach(s StaticGen) error { if reflect.TypeOf(s).Kind() != reflect.Ptr { @@ -329,3 +323,193 @@ func (m FileManager) Runner(event string, path string) error { return nil } + +// DefaultUser is used on New, when no 'base' user is provided. +var DefaultUser = User{ + AllowCommands: true, + AllowEdit: true, + AllowNew: true, + AllowPublish: true, + Commands: []string{}, + Rules: []*Rule{}, + CSS: "", + Admin: true, + Locale: "en", + FileSystem: fileutils.Dir("."), +} + +// User contains the configuration for each user. +type User struct { + // ID is the required primary key with auto increment0 + ID int `storm:"id,increment"` + + // Username is the user username used to login. + Username string `json:"username" storm:"index,unique"` + + // The hashed password. This never reaches the front-end because it's temporarily + // emptied during JSON marshall. + Password string `json:"password"` + + // Tells if this user is an admin. + Admin bool `json:"admin"` + + // FileSystem is the virtual file system the user has access. + FileSystem fileutils.Dir `json:"filesystem"` + + // Rules is an array of access and deny rules. + Rules []*Rule `json:"rules"` + + // Custom styles for this user. + CSS string `json:"css"` + + // Locale is the language of the user. + Locale string `json:"locale"` + + // These indicate if the user can perform certain actions. + AllowNew bool `json:"allowNew"` // Create files and folders + AllowEdit bool `json:"allowEdit"` // Edit/rename files + AllowCommands bool `json:"allowCommands"` // Execute commands + AllowPublish bool `json:"allowPublish"` // Publish content (to use with static gen) + + // Commands is the list of commands the user can execute. + Commands []string `json:"commands"` +} + +// Allowed checks if the user has permission to access a directory/file. +func (u User) Allowed(url string) bool { + var rule *Rule + i := len(u.Rules) - 1 + + for i >= 0 { + rule = u.Rules[i] + + if rule.Regex { + if rule.Regexp.MatchString(url) { + return rule.Allow + } + } else if strings.HasPrefix(url, rule.Path) { + return rule.Allow + } + + i-- + } + + return true +} + +// Rule is a dissalow/allow rule. +type Rule struct { + // Regex indicates if this rule uses Regular Expressions or not. + Regex bool `json:"regex"` + + // Allow indicates if this is an allow rule. Set 'false' to be a disallow rule. + Allow bool `json:"allow"` + + // Path is the corresponding URL path for this rule. + Path string `json:"path"` + + // Regexp is the regular expression. Only use this when 'Regex' was set to true. + Regexp *Regexp `json:"regexp"` +} + +// Regexp is a regular expression wrapper around native regexp. +type Regexp struct { + Raw string `json:"raw"` + regexp *regexp.Regexp +} + +// MatchString checks if this string matches the regular expression. +func (r *Regexp) MatchString(s string) bool { + if r.regexp == nil { + r.regexp = regexp.MustCompile(r.Raw) + } + + return r.regexp.MatchString(s) +} + +// ShareLink is the information needed to build a shareable link. +type ShareLink struct { + Hash string `json:"hash" storm:"id,index"` + Path string `json:"path" storm:"index"` + Expires bool `json:"expires"` + ExpireDate time.Time `json:"expireDate"` +} + +// Store is a collection of the stores needed to get +// and save information. +type Store struct { + Users UsersStore + Config ConfigStore + Share ShareStore +} + +// UsersStore is the interface to manage users. +type UsersStore interface { + Get(id int) (*User, error) + Gets() ([]*User, error) + Save(u *User) error + Update(u *User, fields ...string) error + Delete(id int) error +} + +// ConfigStore is the interface to manage configuration. +type ConfigStore interface { + Get(name string, to interface{}) error + Save(name string, from interface{}) error +} + +// ShareStore is the interface to manage share links. +type ShareStore interface { + Get(hash string) (*ShareLink, error) + GetByPath(path string) ([]*ShareLink, error) + Gets() ([]*ShareLink, error) + Save(s *ShareLink) error + Delete(hash string) error +} + +// StaticGen is a static website generator. +type StaticGen interface { + SettingsPath() string + Name() string + Setup() error + + Hook(c *Context, w http.ResponseWriter, r *http.Request) (int, error) + Preview(c *Context, w http.ResponseWriter, r *http.Request) (int, error) + Publish(c *Context, w http.ResponseWriter, r *http.Request) (int, error) +} + +// Context contains the needed information to make handlers work. +type Context struct { + *FileManager + User *User + File *File + // On API handlers, Router is the APi handler we want. + Router string +} + +// HashPassword generates an hash from a password using bcrypt. +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes), err +} + +// CheckPasswordHash compares a password with an hash to check if they match. +func CheckPasswordHash(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +// GenerateRandomBytes returns securely generated random bytes. +// It will return an fm.Error if the system's secure random +// number generator fails to function correctly, in which +// case the caller should not continue. +func GenerateRandomBytes(n int) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + // Note that err == nil only if we read len(b) bytes. + if err != nil { + return nil, err + } + + return b, nil +} diff --git a/http.go b/http.go deleted file mode 100644 index f27c24ef..00000000 --- a/http.go +++ /dev/null @@ -1,23 +0,0 @@ -package filemanager - -import "net/http" - -// StaticGen is a static website generator. -type StaticGen interface { - SettingsPath() string - Name() string - Setup() error - - Hook(c *Context, w http.ResponseWriter, r *http.Request) (int, error) - Preview(c *Context, w http.ResponseWriter, r *http.Request) (int, error) - Publish(c *Context, w http.ResponseWriter, r *http.Request) (int, error) -} - -// Context contains the needed information to make handlers work. -type Context struct { - *FileManager - User *User - File *File - // On API handlers, Router is the APi handler we want. - Router string -} diff --git a/http/http.go b/http/http.go index 23653720..bfa33df7 100644 --- a/http/http.go +++ b/http/http.go @@ -3,6 +3,7 @@ package http import ( "encoding/json" "html/template" + "log" "net/http" "os" "strings" @@ -12,8 +13,34 @@ import ( fm "github.com/hacdias/filemanager" ) -// ServeHTTP is the main entry point of this HTML application. -func ServeHTTP(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { +// ServeHTTP returns a function compatible with http.HandleFunc. +func ServeHTTP(m *fm.FileManager) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + code, err := serve(&fm.Context{ + FileManager: m, + User: nil, + File: nil, + }, w, r) + + if code >= 400 { + w.WriteHeader(code) + + if err == nil { + txt := http.StatusText(code) + log.Printf("%v: %v %v\n", r.URL.Path, code, txt) + w.Write([]byte(txt)) + } + } + + if err != nil { + log.Print(err) + w.Write([]byte(err.Error())) + } + }) +} + +// serve is the main entry point of this HTML application. +func serve(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { // Checks if the URL contains the baseURL and strips it. Otherwise, it just // returns a 404 fm.Error because we're not supposed to be here! p := strings.TrimPrefix(r.URL.Path, c.BaseURL) diff --git a/staticgen/jekyll.go b/staticgen/jekyll.go index e03fc9ab..4c160458 100644 --- a/staticgen/jekyll.go +++ b/staticgen/jekyll.go @@ -28,6 +28,11 @@ type Jekyll struct { previewPath string } +// Name is the plugin's name. +func (j Jekyll) Name() string { + return "jekyll" +} + // SettingsPath retrieves the correct settings path. func (j Jekyll) SettingsPath() string { return "/_config.yml" diff --git a/testdata/.gitkeep b/testdata/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/user.go b/user.go deleted file mode 100644 index 18267b21..00000000 --- a/user.go +++ /dev/null @@ -1,188 +0,0 @@ -package filemanager - -import ( - "crypto/rand" - "errors" - "regexp" - "strings" - "time" - - "golang.org/x/crypto/bcrypt" - - "github.com/hacdias/fileutils" -) - -var ( - ErrExist = errors.New("the resource already exists") - ErrNotExist = errors.New("the resource does not exist") - ErrEmptyRequest = errors.New("request body is empty") - ErrEmptyPassword = errors.New("password is empty") - ErrEmptyUsername = errors.New("username is empty") - ErrEmptyScope = errors.New("scope is empty") - ErrWrongDataType = errors.New("wrong data type") - ErrInvalidUpdateField = errors.New("invalid field to update") -) - -// DefaultUser is used on New, when no 'base' user is provided. -var DefaultUser = User{ - AllowCommands: true, - AllowEdit: true, - AllowNew: true, - AllowPublish: true, - Commands: []string{}, - Rules: []*Rule{}, - CSS: "", - Admin: true, - Locale: "en", - FileSystem: fileutils.Dir("."), -} - -// User contains the configuration for each user. -type User struct { - // ID is the required primary key with auto increment0 - ID int `storm:"id,increment"` - - // Username is the user username used to login. - Username string `json:"username" storm:"index,unique"` - - // The hashed password. This never reaches the front-end because it's temporarily - // emptied during JSON marshall. - Password string `json:"password"` - - // Tells if this user is an admin. - Admin bool `json:"admin"` - - // FileSystem is the virtual file system the user has access. - FileSystem fileutils.Dir `json:"filesystem"` - - // Rules is an array of access and deny rules. - Rules []*Rule `json:"rules"` - - // Custom styles for this user. - CSS string `json:"css"` - - // Locale is the language of the user. - Locale string `json:"locale"` - - // These indicate if the user can perform certain actions. - AllowNew bool `json:"allowNew"` // Create files and folders - AllowEdit bool `json:"allowEdit"` // Edit/rename files - AllowCommands bool `json:"allowCommands"` // Execute commands - AllowPublish bool `json:"allowPublish"` // Publish content (to use with static gen) - - // Commands is the list of commands the user can execute. - Commands []string `json:"commands"` -} - -// Rule is a dissalow/allow rule. -type Rule struct { - // Regex indicates if this rule uses Regular Expressions or not. - Regex bool `json:"regex"` - - // Allow indicates if this is an allow rule. Set 'false' to be a disallow rule. - Allow bool `json:"allow"` - - // Path is the corresponding URL path for this rule. - Path string `json:"path"` - - // Regexp is the regular expression. Only use this when 'Regex' was set to true. - Regexp *Regexp `json:"regexp"` -} - -// Regexp is a regular expression wrapper around native regexp. -type Regexp struct { - Raw string `json:"raw"` - regexp *regexp.Regexp -} - -// Allowed checks if the user has permission to access a directory/file. -func (u User) Allowed(url string) bool { - var rule *Rule - i := len(u.Rules) - 1 - - for i >= 0 { - rule = u.Rules[i] - - if rule.Regex { - if rule.Regexp.MatchString(url) { - return rule.Allow - } - } else if strings.HasPrefix(url, rule.Path) { - return rule.Allow - } - - i-- - } - - return true -} - -// MatchString checks if this string matches the regular expression. -func (r *Regexp) MatchString(s string) bool { - if r.regexp == nil { - r.regexp = regexp.MustCompile(r.Raw) - } - - return r.regexp.MatchString(s) -} - -type ShareLink struct { - Hash string `json:"hash" storm:"id,index"` - Path string `json:"path" storm:"index"` - Expires bool `json:"expires"` - ExpireDate time.Time `json:"expireDate"` -} - -type Store struct { - Users UsersStore - Config ConfigStore - Share ShareStore -} - -type UsersStore interface { - Get(id int) (*User, error) - Gets() ([]*User, error) - Save(u *User) error - Update(u *User, fields ...string) error - Delete(id int) error -} - -type ConfigStore interface { - Get(name string, to interface{}) error - Save(name string, from interface{}) error -} - -type ShareStore interface { - Get(hash string) (*ShareLink, error) - GetByPath(path string) ([]*ShareLink, error) - Gets() ([]*ShareLink, error) - Save(s *ShareLink) error - Delete(hash string) error -} - -// HashPassword generates an hash from a password using bcrypt. -func HashPassword(password string) (string, error) { - bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - return string(bytes), err -} - -// CheckPasswordHash compares a password with an hash to check if they match. -func CheckPasswordHash(password, hash string) bool { - err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) - return err == nil -} - -// GenerateRandomBytes returns securely generated random bytes. -// It will return an fm.Error if the system's secure random -// number generator fails to function correctly, in which -// case the caller should not continue. -func GenerateRandomBytes(n int) ([]byte, error) { - b := make([]byte, n) - _, err := rand.Read(b) - // Note that err == nil only if we read len(b) bytes. - if err != nil { - return nil, err - } - - return b, nil -}