Skip to content

Commit

Permalink
allow limiting access by passwords (fix #194)
Browse files Browse the repository at this point in the history
  • Loading branch information
stek29 committed Feb 17, 2020
1 parent cf0a30f commit b63274a
Show file tree
Hide file tree
Showing 12 changed files with 197 additions and 34 deletions.
138 changes: 138 additions & 0 deletions access.go
@@ -0,0 +1,138 @@
package main

import (
"encoding/json"
"errors"
"net/http"
"regexp"
"strings"
"time"

"github.com/andreimarcu/linx-server/backends"
"github.com/flosch/pongo2"
"github.com/zenazn/goji/web"
)

type accessKeySource int

const (
accessKeySourceNone accessKeySource = iota
accessKeySourceCookie
accessKeySourceHeader
accessKeySourceForm
accessKeySourceQuery
)

const accessKeyHeaderName = "Linx-Access-Key"
const accessKeyParamName = "access_key"

var (
errInvalidAccessKey = errors.New("invalid access key")

cliUserAgentRe = regexp.MustCompile("(?i)(lib)?curl|wget")
)

func checkAccessKey(r *http.Request, metadata *backends.Metadata) (accessKeySource, error) {
key := metadata.AccessKey
if key == "" {
return accessKeySourceNone, nil
}

cookieKey, err := r.Cookie(accessKeyHeaderName)
if err == nil {
if cookieKey.Value == key {
return accessKeySourceCookie, nil
}
return accessKeySourceCookie, errInvalidAccessKey
}

headerKey := r.Header.Get(accessKeyHeaderName)
if headerKey == key {
return accessKeySourceHeader, nil
} else if headerKey != "" {
return accessKeySourceHeader, errInvalidAccessKey
}

formKey := r.PostFormValue(accessKeyParamName)
if formKey == key {
return accessKeySourceForm, nil
} else if formKey != "" {
return accessKeySourceForm, errInvalidAccessKey
}

queryKey := r.URL.Query().Get(accessKeyParamName)
if queryKey == key {
return accessKeySourceQuery, nil
} else if formKey != "" {
return accessKeySourceQuery, errInvalidAccessKey
}

return accessKeySourceNone, errInvalidAccessKey
}

func setAccessKeyCookies(w http.ResponseWriter, domain, fileName, value string, expires time.Time) {
cookie := http.Cookie{
Name: accessKeyHeaderName,
Value: value,
HttpOnly: true,
Domain: domain,
Expires: expires,
}

cookie.Path = Config.sitePath + fileName
http.SetCookie(w, &cookie)

cookie.Path = Config.sitePath + Config.selifPath + fileName
http.SetCookie(w, &cookie)
}

func fileAccessHandler(c web.C, w http.ResponseWriter, r *http.Request) {
if !Config.noDirectAgents && cliUserAgentRe.MatchString(r.Header.Get("User-Agent")) && !strings.EqualFold("application/json", r.Header.Get("Accept")) {
fileServeHandler(c, w, r)
return
}

fileName := c.URLParams["name"]

metadata, err := checkFile(fileName)
if err == backends.NotFoundErr {
notFoundHandler(c, w, r)
return
} else if err != nil {
oopsHandler(c, w, r, RespAUTO, "Corrupt metadata.")
return
}

if src, err := checkAccessKey(r, &metadata); err != nil {
// remove invalid cookie
if src == accessKeySourceCookie {
setAccessKeyCookies(w, getSiteURL(r), fileName, "", time.Unix(0, 0))
}

if strings.EqualFold("application/json", r.Header.Get("Accept")) {
dec := json.NewEncoder(w)
_ = dec.Encode(map[string]string{
"error": errInvalidAccessKey.Error(),
})

return
}

_ = renderTemplate(Templates["access.html"], pongo2.Context{
"filename": fileName,
"accesspath": fileName,
}, r, w)

return
}

if metadata.AccessKey != "" {
var expiry time.Time
if Config.accessKeyCookieExpiry != 0 {
expiry = time.Now().Add(time.Duration(Config.accessKeyCookieExpiry) * time.Second)
}
setAccessKeyCookies(w, getSiteURL(r), fileName, metadata.AccessKey, expiry)
}

fileDisplayHandler(c, w, r, fileName, metadata)
}
20 changes: 12 additions & 8 deletions backends/localfs/localfs.go
Expand Up @@ -19,6 +19,7 @@ type LocalfsBackend struct {

type MetadataJSON struct {
DeleteKey string `json:"delete_key"`
AccessKey string `json:"access_key,omitempty"`
Sha256sum string `json:"sha256sum"`
Mimetype string `json:"mimetype"`
Size int64 `json:"size"`
Expand Down Expand Up @@ -57,6 +58,7 @@ func (b LocalfsBackend) Head(key string) (metadata backends.Metadata, err error)
}

metadata.DeleteKey = mjson.DeleteKey
metadata.AccessKey = mjson.AccessKey
metadata.Mimetype = mjson.Mimetype
metadata.ArchiveFiles = mjson.ArchiveFiles
metadata.Sha256sum = mjson.Sha256sum
Expand Down Expand Up @@ -84,12 +86,13 @@ func (b LocalfsBackend) writeMetadata(key string, metadata backends.Metadata) er
metaPath := path.Join(b.metaPath, key)

mjson := MetadataJSON{
DeleteKey: metadata.DeleteKey,
Mimetype: metadata.Mimetype,
DeleteKey: metadata.DeleteKey,
AccessKey: metadata.AccessKey,
Mimetype: metadata.Mimetype,
ArchiveFiles: metadata.ArchiveFiles,
Sha256sum: metadata.Sha256sum,
Expiry: metadata.Expiry.Unix(),
Size: metadata.Size,
Sha256sum: metadata.Sha256sum,
Expiry: metadata.Expiry.Unix(),
Size: metadata.Size,
}

dst, err := os.Create(metaPath)
Expand All @@ -108,7 +111,7 @@ func (b LocalfsBackend) writeMetadata(key string, metadata backends.Metadata) er
return nil
}

func (b LocalfsBackend) Put(key string, r io.Reader, expiry time.Time, deleteKey string) (m backends.Metadata, err error) {
func (b LocalfsBackend) Put(key string, r io.Reader, expiry time.Time, deleteKey, accessKey string) (m backends.Metadata, err error) {
filePath := path.Join(b.filesPath, key)

dst, err := os.Create(filePath)
Expand All @@ -126,16 +129,17 @@ func (b LocalfsBackend) Put(key string, r io.Reader, expiry time.Time, deleteKey
return m, err
}

dst.Seek(0 ,0)
dst.Seek(0, 0)
m, err = helpers.GenerateMetadata(dst)
if err != nil {
os.Remove(filePath)
return
}
dst.Seek(0 ,0)
dst.Seek(0, 0)

m.Expiry = expiry
m.DeleteKey = deleteKey
m.AccessKey = accessKey
m.ArchiveFiles, _ = helpers.ListArchiveFiles(m.Mimetype, m.Size, dst)

err = b.writeMetadata(key, m)
Expand Down
1 change: 1 addition & 0 deletions backends/meta.go
Expand Up @@ -7,6 +7,7 @@ import (

type Metadata struct {
DeleteKey string
AccessKey string
Sha256sum string
Mimetype string
Size int64
Expand Down
9 changes: 8 additions & 1 deletion backends/s3/s3.go
Expand Up @@ -86,6 +86,7 @@ func mapMetadata(m backends.Metadata) map[string]*string {
"Size": aws.String(strconv.FormatInt(m.Size, 10)),
"Mimetype": aws.String(m.Mimetype),
"Sha256sum": aws.String(m.Sha256sum),
"AccessKey": aws.String(m.AccessKey),
}
}

Expand All @@ -104,10 +105,15 @@ func unmapMetadata(input map[string]*string) (m backends.Metadata, err error) {
m.DeleteKey = aws.StringValue(input["Delete_key"])
m.Mimetype = aws.StringValue(input["Mimetype"])
m.Sha256sum = aws.StringValue(input["Sha256sum"])

if key, ok := input["AccessKey"]; ok {
m.AccessKey = aws.StringValue(key)
}

return
}

func (b S3Backend) Put(key string, r io.Reader, expiry time.Time, deleteKey string) (m backends.Metadata, err error) {
func (b S3Backend) Put(key string, r io.Reader, expiry time.Time, deleteKey, accessKey string) (m backends.Metadata, err error) {
tmpDst, err := ioutil.TempFile("", "linx-server-upload")
if err != nil {
return m, err
Expand All @@ -133,6 +139,7 @@ func (b S3Backend) Put(key string, r io.Reader, expiry time.Time, deleteKey stri
}
m.Expiry = expiry
m.DeleteKey = deleteKey
m.AccessKey = accessKey
// XXX: we may not be able to write this to AWS easily
//m.ArchiveFiles, _ = helpers.ListArchiveFiles(m.Mimetype, m.Size, tmpDst)

Expand Down
2 changes: 1 addition & 1 deletion backends/storage.go
Expand Up @@ -11,7 +11,7 @@ type StorageBackend interface {
Exists(key string) (bool, error)
Head(key string) (Metadata, error)
Get(key string) (Metadata, io.ReadCloser, error)
Put(key string, r io.Reader, expiry time.Time, deleteKey string) (Metadata, error)
Put(key string, r io.Reader, expiry time.Time, deleteKey, accessKey string) (Metadata, error)
PutMetadata(key string, m Metadata) error
Size(key string) (int64, error)
}
Expand Down
22 changes: 2 additions & 20 deletions display.go
Expand Up @@ -5,7 +5,6 @@ import (
"io/ioutil"
"net/http"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
Expand All @@ -21,24 +20,7 @@ import (

const maxDisplayFileSizeBytes = 1024 * 512

var cliUserAgentRe = regexp.MustCompile("(?i)(lib)?curl|wget")

func fileDisplayHandler(c web.C, w http.ResponseWriter, r *http.Request) {
if !Config.noDirectAgents && cliUserAgentRe.MatchString(r.Header.Get("User-Agent")) && !strings.EqualFold("application/json", r.Header.Get("Accept")) {
fileServeHandler(c, w, r)
return
}

fileName := c.URLParams["name"]

metadata, err := checkFile(fileName)
if err == backends.NotFoundErr {
notFoundHandler(c, w, r)
return
} else if err != nil {
oopsHandler(c, w, r, RespAUTO, "Corrupt metadata.")
return
}
func fileDisplayHandler(c web.C, w http.ResponseWriter, r *http.Request, fileName string, metadata backends.Metadata) {
var expiryHuman string
if metadata.Expiry != expiry.NeverExpire {
expiryHuman = humanize.RelTime(time.Now(), metadata.Expiry, "", "")
Expand Down Expand Up @@ -130,7 +112,7 @@ func fileDisplayHandler(c web.C, w http.ResponseWriter, r *http.Request) {
tpl = Templates["display/file.html"]
}

err = renderTemplate(tpl, pongo2.Context{
err := renderTemplate(tpl, pongo2.Context{
"mime": metadata.Mimetype,
"filename": fileName,
"size": sizeHuman,
Expand Down
10 changes: 10 additions & 0 deletions fileserve.go
Expand Up @@ -27,6 +27,16 @@ func fileServeHandler(c web.C, w http.ResponseWriter, r *http.Request) {
return
}

if src, err := checkAccessKey(r, &metadata); err != nil {
// remove invalid cookie
if src == accessKeySourceCookie {
setAccessKeyCookies(w, getSiteURL(r), fileName, "", time.Unix(0, 0))
}
unauthorizedHandler(c, w, r)

return
}

if !Config.allowHotlink {
referer := r.Header.Get("Referer")
u, _ := url.Parse(referer)
Expand Down
7 changes: 5 additions & 2 deletions server.go
Expand Up @@ -15,7 +15,7 @@ import (
"syscall"
"time"

"github.com/GeertJohan/go.rice"
rice "github.com/GeertJohan/go.rice"
"github.com/andreimarcu/linx-server/backends"
"github.com/andreimarcu/linx-server/backends/localfs"
"github.com/andreimarcu/linx-server/backends/s3"
Expand Down Expand Up @@ -68,6 +68,7 @@ var Config struct {
s3Bucket string
s3ForcePathStyle bool
forceRandomFilename bool
accessKeyCookieExpiry uint64
}

var Templates = make(map[string]*pongo2.Template)
Expand Down Expand Up @@ -200,7 +201,8 @@ func setup() *web.Mux {
mux.Get(Config.sitePath+"static/*", staticHandler)
mux.Get(Config.sitePath+"favicon.ico", staticHandler)
mux.Get(Config.sitePath+"robots.txt", staticHandler)
mux.Get(nameRe, fileDisplayHandler)
mux.Get(nameRe, fileAccessHandler)
mux.Post(nameRe, fileAccessHandler)
mux.Get(selifRe, fileServeHandler)
mux.Get(selifIndexRe, unauthorizedHandler)
mux.Get(torrentRe, fileTorrentHandler)
Expand Down Expand Up @@ -273,6 +275,7 @@ func main() {
"Force path-style addressing for S3 (e.g. https://s3.amazonaws.com/linx/example.txt)")
flag.BoolVar(&Config.forceRandomFilename, "force-random-filename", false,
"Force all uploads to use a random filename")
flag.Uint64Var(&Config.accessKeyCookieExpiry, "access-cookie-expiry", 0, "Expiration time for access key cookies in seconds (set 0 to use session cookies)")

iniflags.Parse()

Expand Down
3 changes: 2 additions & 1 deletion templates.go
Expand Up @@ -8,7 +8,7 @@ import (
"path/filepath"
"strings"

"github.com/GeertJohan/go.rice"
rice "github.com/GeertJohan/go.rice"
"github.com/flosch/pongo2"
)

Expand Down Expand Up @@ -51,6 +51,7 @@ func populateTemplatesMap(tSet *pongo2.TemplateSet, tMap map[string]*pongo2.Temp
"401.html",
"404.html",
"oops.html",
"access.html",

"display/audio.html",
"display/image.html",
Expand Down
11 changes: 11 additions & 0 deletions templates/access.html
@@ -0,0 +1,11 @@
{% extends "base.html" %}

{% block content %}
<div id="access">
<form action="{{ unlockpath }}" method="POST" enctype="multipart/form-data">
{{ filename }} is protected with password <br />
<input id="access_key" name="access_key" type="password" />
<input id="submitbtn" type="submit" value="Unlock">
</form>
</div>
{% endblock %}
1 change: 1 addition & 0 deletions templates/index.html
Expand Up @@ -27,6 +27,7 @@
</select>
</label>
</div>
<label><input name="access_key" type="text"/> Require password to access</label>
</div>
<div class="clear"></div>
</form>
Expand Down

0 comments on commit b63274a

Please sign in to comment.