Skip to content

Commit

Permalink
Merge pull request #44 from patrickhener/filebased-access
Browse files Browse the repository at this point in the history
Implement ACLs as request by Issue #43
  • Loading branch information
patrickhener authored Jun 21, 2023
2 parents 1d120cf + a149e53 commit 7f7b0fc
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 6 deletions.
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
![Version](https://img.shields.io/badge/Version-v0.3.4-green)
![Version](https://img.shields.io/badge/Version-v0.3.5-green)
[![GitHub](https://img.shields.io/github/license/patrickhener/goshs)](https://github.com/patrickhener/goshs/blob/master/LICENSE)
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/patrickhener/goshs)
[![GitHub issues](https://img.shields.io/github/issues-raw/patrickhener/goshs)](https://github.com/patrickhener/goshs/issues)
Expand Down Expand Up @@ -33,6 +33,10 @@ goshs is a replacement for Python's `SimpleHTTPServer`. It allows uploading and
* Light Mode
* Command Line
* Run Commands on the system hosting `goshs`
* File Based ACLs
* You can place a `.goshs` in any folder to apply custom ACLs
* You can apply custom basic auth per folder
* You can hide files from the listing per folder

# Installation

Expand Down Expand Up @@ -83,6 +87,7 @@ TLS options:

Authentication options:
-b, --basic-auth Use basic authentication (user:pass - user can be empty)
-H, --hash Hash a password for file based ACLs

Misc options:
-u --user Drop privs to user (unix only) (default: current user)
Expand Down Expand Up @@ -222,6 +227,30 @@ CLI mode will let you run commands on the system hosting `goshs` and return the
`goshs -b secret-user:$up3r$3cur3 -s -ss -c`
**File Based ACLs**
You can apply file based access control lists per folder by placing a file called `.goshs` in that folder. The files content is like:
```json
{
"auth":"<user>:<hash>",
"hide":[
"file1",
"file2",
"folder/"
]
}
```
The hash you have to use can be generated with `goshs -H` or `goshs --hash`. This will generate a bCrypt hash. The username can be left empty.
```bash
goshs --hash
Enter password: *******
Hash: $2a$14$hh50ncgjLAOQT3KI1RlVYus3gMecE4/Ul2HakUp6iiBCnl2c5M0da
```
The `hide` mode will only **hide** the files from the listing but will **not restrict access** to it if called directly.
# Credits
A special thank you goes to *sc0tfree* for inspiring this project with his project [updog](https://github.com/sc0tfree/updog) written in Python.
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ go 1.19
require (
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.4.2
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef
github.com/sirupsen/logrus v1.8.1
golang.org/x/crypto v0.10.0
golang.org/x/net v0.10.0
)

require golang.org/x/sys v0.8.0 // indirect
require (
golang.org/x/sys v0.9.0 // indirect
golang.org/x/term v0.9.0 // indirect
)
10 changes: 8 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM=
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
37 changes: 37 additions & 0 deletions httpserver/filebased.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package httpserver

import (
"encoding/json"
"io"
"io/fs"
"os"
"path/filepath"
)

func (fs *FileServer) findSpecialFile(fis []fs.FileInfo, file *os.File) (configFile, error) {
var config configFile

for _, fi := range fis {
if fi.Name() == ".goshs" {
openFile := filepath.Join(file.Name(), fi.Name())

configFileDisk, err := os.Open(openFile)
if err != nil {
return config, err
}

configFileBytes, err := io.ReadAll(configFileDisk)
if err != nil {
return config, err
}

if err := json.Unmarshal(configFileBytes, &config); err != nil {
return config, err
}

return config, nil
}
}

return config, nil
}
47 changes: 47 additions & 0 deletions httpserver/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,40 @@ func (fs *FileServer) processDir(w http.ResponseWriter, req *http.Request, file
// Cleanup for Windows Paths
relpath = strings.TrimLeft(relpath, "\\")

// File Based Access Flag
config, err := fs.findSpecialFile(fis, file)
if err != nil {
logger.Errorf("error reading file based access config: %+v", err)
}

// Apply Custom Auth if there
if config.Auth != "" {
w.Header().Set("WWW-Authenticate", `Basic realm="Filebased Restricted"`)

username, password, authOK := req.BasicAuth()
if !authOK {
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}

user := strings.Split(config.Auth, ":")[0]
passwordHash := strings.Split(config.Auth, ":")[1]

if username != user || !checkPasswordHash(password, passwordHash) {
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}
}

// Create empty slice
items := make([]item, 0, len(fis))
// Iterate over FileInfo of dir
for _, fi := range fis {
if fi.Name() == ".goshs" {
logger.Debug(".goshs detected and therefore applying")
// Do not add it to items
continue
}
item := item{}
// Need to set this up here for directories to work
item.Name = fi.Name()
Expand Down Expand Up @@ -155,6 +185,13 @@ func (fs *FileServer) processDir(w http.ResponseWriter, req *http.Request, file
items = append(items, item)
}

// Remove 'hide' files from items
if len(config.Hide) > 0 {
for _, i := range config.Hide {
items = removeItem(items, i)
}
}

// Sort slice all lowercase
sort.Slice(items, func(i, j int) bool {
return strings.ToLower(items[i].Name) < strings.ToLower(items[j].Name)
Expand Down Expand Up @@ -252,6 +289,16 @@ func (fs *FileServer) sendFile(w http.ResponseWriter, req *http.Request, file *o
fs.handleError(w, req, fmt.Errorf("%s", "Download not allowed due to 'upload only' option"), http.StatusForbidden)
return
}

// Never serve .goshs file and return same error message if it was not there
// This way it is also not possible to enumerate
pathSplit := strings.Split(req.URL.Path, "/")
filename := pathSplit[len(pathSplit)-1]
if filename == ".goshs" {
fs.handleError(w, req, fmt.Errorf("open %s: no such file or directory", file.Name()), 404)
return
}

// Extract download parameter
download := req.URL.Query()
if _, ok := download["download"]; ok {
Expand Down
13 changes: 13 additions & 0 deletions httpserver/helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package httpserver

func removeItem(sSlice []item, item string) []item {
index := 0

for idx, sliceItem := range sSlice {
if item == sliceItem.Name {
index = idx
}
}

return append(sSlice[:index], sSlice[index+1:]...)
}
11 changes: 10 additions & 1 deletion httpserver/middleware.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package httpserver

import "net/http"
import (
"net/http"

"golang.org/x/crypto/bcrypt"
)

// BasicAuthMiddleware is a middleware to handle the basic auth
func (fs *FileServer) BasicAuthMiddleware(next http.Handler) http.Handler {
Expand All @@ -21,3 +25,8 @@ func (fs *FileServer) BasicAuthMiddleware(next http.Handler) http.Handler {
next.ServeHTTP(w, r)
})
}

func checkPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
5 changes: 5 additions & 0 deletions httpserver/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,8 @@ type httperror struct {
GoshsVersion string
Statics template.FuncMap
}

type configFile struct {
Auth string `json:"auth"`
Hide []string `json:"hide"`
}
10 changes: 9 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
"github.com/patrickhener/goshs/utils"
)

const goshsVersion = "v0.3.4"
const goshsVersion = "v0.3.5"

var (
port = 8000
Expand Down Expand Up @@ -63,6 +63,7 @@ TLS options:
Authentication options:
-b, --basic-auth Use basic authentication (user:pass - user can be empty)
-H, --hash Hash a password for file based ACLs
Misc options:
-u --user Drop privs to user (unix only) (default: current user)
Expand Down Expand Up @@ -120,6 +121,8 @@ func init() {
flag.StringVar(&dropuser, "user", dropuser, "user")
flag.BoolVar(&cli, "c", cli, "cli")
flag.BoolVar(&cli, "cli", cli, "cli")
hash := flag.Bool("H", false, "hash")
hashLong := flag.Bool("hash", false, "hash")
version := flag.Bool("v", false, "goshs version")

flag.Usage = usage()
Expand All @@ -131,6 +134,11 @@ func init() {
os.Exit(0)
}

if *hash || *hashLong {
utils.HashPassword()
os.Exit(1)
}

// Check if interface name was provided as -i
// If so, resolve to ip address of interface
if !strings.Contains(ip, ".") {
Expand Down
18 changes: 18 additions & 0 deletions utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"net"
"strings"

"github.com/howeyc/gopass"
"github.com/patrickhener/goshs/logger"
"golang.org/x/crypto/bcrypt"
)

// ByteCountDecimal generates human readable file sizes and returns a string
Expand Down Expand Up @@ -91,3 +93,19 @@ func GetAllIPAdresses() (map[string]string, error) {
}
return ifaceAddress, nil
}

// HashPassword will take a plaintext masked password and return a bcrypt hash
// This is meant to be used with the filebased access via .goshs file
func HashPassword() {
fmt.Printf("Enter password: ")
password, err := gopass.GetPasswdMasked()
if err != nil {
logger.Fatalf("error reading password from stdin: %+v", err)
}

bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
if err != nil {
logger.Fatalf("error hashing password: %+v", err)
}
fmt.Printf("Hash: %s\n", string(bytes))
}

0 comments on commit 7f7b0fc

Please sign in to comment.