allow limiting access by passwords (fix #194)

This commit is contained in:
Viktor Oreshkin 2020-02-17 17:58:56 +03:00
parent cf0a30f46a
commit b63274ad01
12 changed files with 197 additions and 34 deletions

138
access.go Normal file
View File

@ -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)
}

View File

@ -19,6 +19,7 @@ type LocalfsBackend struct {
type MetadataJSON struct { type MetadataJSON struct {
DeleteKey string `json:"delete_key"` DeleteKey string `json:"delete_key"`
AccessKey string `json:"access_key,omitempty"`
Sha256sum string `json:"sha256sum"` Sha256sum string `json:"sha256sum"`
Mimetype string `json:"mimetype"` Mimetype string `json:"mimetype"`
Size int64 `json:"size"` Size int64 `json:"size"`
@ -57,6 +58,7 @@ func (b LocalfsBackend) Head(key string) (metadata backends.Metadata, err error)
} }
metadata.DeleteKey = mjson.DeleteKey metadata.DeleteKey = mjson.DeleteKey
metadata.AccessKey = mjson.AccessKey
metadata.Mimetype = mjson.Mimetype metadata.Mimetype = mjson.Mimetype
metadata.ArchiveFiles = mjson.ArchiveFiles metadata.ArchiveFiles = mjson.ArchiveFiles
metadata.Sha256sum = mjson.Sha256sum metadata.Sha256sum = mjson.Sha256sum
@ -85,6 +87,7 @@ func (b LocalfsBackend) writeMetadata(key string, metadata backends.Metadata) er
mjson := MetadataJSON{ mjson := MetadataJSON{
DeleteKey: metadata.DeleteKey, DeleteKey: metadata.DeleteKey,
AccessKey: metadata.AccessKey,
Mimetype: metadata.Mimetype, Mimetype: metadata.Mimetype,
ArchiveFiles: metadata.ArchiveFiles, ArchiveFiles: metadata.ArchiveFiles,
Sha256sum: metadata.Sha256sum, Sha256sum: metadata.Sha256sum,
@ -108,7 +111,7 @@ func (b LocalfsBackend) writeMetadata(key string, metadata backends.Metadata) er
return nil 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) filePath := path.Join(b.filesPath, key)
dst, err := os.Create(filePath) dst, err := os.Create(filePath)
@ -126,16 +129,17 @@ func (b LocalfsBackend) Put(key string, r io.Reader, expiry time.Time, deleteKey
return m, err return m, err
} }
dst.Seek(0 ,0) dst.Seek(0, 0)
m, err = helpers.GenerateMetadata(dst) m, err = helpers.GenerateMetadata(dst)
if err != nil { if err != nil {
os.Remove(filePath) os.Remove(filePath)
return return
} }
dst.Seek(0 ,0) dst.Seek(0, 0)
m.Expiry = expiry m.Expiry = expiry
m.DeleteKey = deleteKey m.DeleteKey = deleteKey
m.AccessKey = accessKey
m.ArchiveFiles, _ = helpers.ListArchiveFiles(m.Mimetype, m.Size, dst) m.ArchiveFiles, _ = helpers.ListArchiveFiles(m.Mimetype, m.Size, dst)
err = b.writeMetadata(key, m) err = b.writeMetadata(key, m)

View File

@ -7,6 +7,7 @@ import (
type Metadata struct { type Metadata struct {
DeleteKey string DeleteKey string
AccessKey string
Sha256sum string Sha256sum string
Mimetype string Mimetype string
Size int64 Size int64

View File

@ -86,6 +86,7 @@ func mapMetadata(m backends.Metadata) map[string]*string {
"Size": aws.String(strconv.FormatInt(m.Size, 10)), "Size": aws.String(strconv.FormatInt(m.Size, 10)),
"Mimetype": aws.String(m.Mimetype), "Mimetype": aws.String(m.Mimetype),
"Sha256sum": aws.String(m.Sha256sum), "Sha256sum": aws.String(m.Sha256sum),
"AccessKey": aws.String(m.AccessKey),
} }
} }
@ -104,10 +105,15 @@ func unmapMetadata(input map[string]*string) (m backends.Metadata, err error) {
m.DeleteKey = aws.StringValue(input["Delete_key"]) m.DeleteKey = aws.StringValue(input["Delete_key"])
m.Mimetype = aws.StringValue(input["Mimetype"]) m.Mimetype = aws.StringValue(input["Mimetype"])
m.Sha256sum = aws.StringValue(input["Sha256sum"]) m.Sha256sum = aws.StringValue(input["Sha256sum"])
if key, ok := input["AccessKey"]; ok {
m.AccessKey = aws.StringValue(key)
}
return 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") tmpDst, err := ioutil.TempFile("", "linx-server-upload")
if err != nil { if err != nil {
return m, err return m, err
@ -133,6 +139,7 @@ func (b S3Backend) Put(key string, r io.Reader, expiry time.Time, deleteKey stri
} }
m.Expiry = expiry m.Expiry = expiry
m.DeleteKey = deleteKey m.DeleteKey = deleteKey
m.AccessKey = accessKey
// XXX: we may not be able to write this to AWS easily // XXX: we may not be able to write this to AWS easily
//m.ArchiveFiles, _ = helpers.ListArchiveFiles(m.Mimetype, m.Size, tmpDst) //m.ArchiveFiles, _ = helpers.ListArchiveFiles(m.Mimetype, m.Size, tmpDst)

View File

@ -11,7 +11,7 @@ type StorageBackend interface {
Exists(key string) (bool, error) Exists(key string) (bool, error)
Head(key string) (Metadata, error) Head(key string) (Metadata, error)
Get(key string) (Metadata, io.ReadCloser, 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 PutMetadata(key string, m Metadata) error
Size(key string) (int64, error) Size(key string) (int64, error)
} }

View File

@ -5,7 +5,6 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"path/filepath" "path/filepath"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -21,24 +20,7 @@ import (
const maxDisplayFileSizeBytes = 1024 * 512 const maxDisplayFileSizeBytes = 1024 * 512
var cliUserAgentRe = regexp.MustCompile("(?i)(lib)?curl|wget") func fileDisplayHandler(c web.C, w http.ResponseWriter, r *http.Request, fileName string, metadata backends.Metadata) {
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
}
var expiryHuman string var expiryHuman string
if metadata.Expiry != expiry.NeverExpire { if metadata.Expiry != expiry.NeverExpire {
expiryHuman = humanize.RelTime(time.Now(), metadata.Expiry, "", "") expiryHuman = humanize.RelTime(time.Now(), metadata.Expiry, "", "")
@ -130,7 +112,7 @@ func fileDisplayHandler(c web.C, w http.ResponseWriter, r *http.Request) {
tpl = Templates["display/file.html"] tpl = Templates["display/file.html"]
} }
err = renderTemplate(tpl, pongo2.Context{ err := renderTemplate(tpl, pongo2.Context{
"mime": metadata.Mimetype, "mime": metadata.Mimetype,
"filename": fileName, "filename": fileName,
"size": sizeHuman, "size": sizeHuman,

View File

@ -27,6 +27,16 @@ func fileServeHandler(c web.C, w http.ResponseWriter, r *http.Request) {
return 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 { if !Config.allowHotlink {
referer := r.Header.Get("Referer") referer := r.Header.Get("Referer")
u, _ := url.Parse(referer) u, _ := url.Parse(referer)

View File

@ -15,7 +15,7 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/GeertJohan/go.rice" rice "github.com/GeertJohan/go.rice"
"github.com/andreimarcu/linx-server/backends" "github.com/andreimarcu/linx-server/backends"
"github.com/andreimarcu/linx-server/backends/localfs" "github.com/andreimarcu/linx-server/backends/localfs"
"github.com/andreimarcu/linx-server/backends/s3" "github.com/andreimarcu/linx-server/backends/s3"
@ -68,6 +68,7 @@ var Config struct {
s3Bucket string s3Bucket string
s3ForcePathStyle bool s3ForcePathStyle bool
forceRandomFilename bool forceRandomFilename bool
accessKeyCookieExpiry uint64
} }
var Templates = make(map[string]*pongo2.Template) var Templates = make(map[string]*pongo2.Template)
@ -200,7 +201,8 @@ func setup() *web.Mux {
mux.Get(Config.sitePath+"static/*", staticHandler) mux.Get(Config.sitePath+"static/*", staticHandler)
mux.Get(Config.sitePath+"favicon.ico", staticHandler) mux.Get(Config.sitePath+"favicon.ico", staticHandler)
mux.Get(Config.sitePath+"robots.txt", 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(selifRe, fileServeHandler)
mux.Get(selifIndexRe, unauthorizedHandler) mux.Get(selifIndexRe, unauthorizedHandler)
mux.Get(torrentRe, fileTorrentHandler) mux.Get(torrentRe, fileTorrentHandler)
@ -273,6 +275,7 @@ func main() {
"Force path-style addressing for S3 (e.g. https://s3.amazonaws.com/linx/example.txt)") "Force path-style addressing for S3 (e.g. https://s3.amazonaws.com/linx/example.txt)")
flag.BoolVar(&Config.forceRandomFilename, "force-random-filename", false, flag.BoolVar(&Config.forceRandomFilename, "force-random-filename", false,
"Force all uploads to use a random filename") "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() iniflags.Parse()

View File

@ -8,7 +8,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/GeertJohan/go.rice" rice "github.com/GeertJohan/go.rice"
"github.com/flosch/pongo2" "github.com/flosch/pongo2"
) )
@ -51,6 +51,7 @@ func populateTemplatesMap(tSet *pongo2.TemplateSet, tMap map[string]*pongo2.Temp
"401.html", "401.html",
"404.html", "404.html",
"oops.html", "oops.html",
"access.html",
"display/audio.html", "display/audio.html",
"display/image.html", "display/image.html",

11
templates/access.html Normal file
View File

@ -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 %}

View File

@ -27,6 +27,7 @@
</select> </select>
</label> </label>
</div> </div>
<label><input name="access_key" type="text"/> Require password to access</label>
</div> </div>
<div class="clear"></div> <div class="clear"></div>
</form> </form>

View File

@ -40,6 +40,7 @@ type UploadRequest struct {
expiry time.Duration // Seconds until expiry, 0 = never expiry time.Duration // Seconds until expiry, 0 = never
deleteKey string // Empty string if not defined deleteKey string // Empty string if not defined
randomBarename bool randomBarename bool
accessKey string // Empty string if not defined
} }
// Metadata associated with a file as it would actually be stored // Metadata associated with a file as it would actually be stored
@ -88,6 +89,7 @@ func uploadPostHandler(c web.C, w http.ResponseWriter, r *http.Request) {
} }
upReq.expiry = parseExpiry(r.PostFormValue("expires")) upReq.expiry = parseExpiry(r.PostFormValue("expires"))
upReq.accessKey = r.PostFormValue(accessKeyParamName)
if r.PostFormValue("randomize") == "true" { if r.PostFormValue("randomize") == "true" {
upReq.randomBarename = true upReq.randomBarename = true
@ -181,6 +183,7 @@ func uploadRemote(c web.C, w http.ResponseWriter, r *http.Request) {
upReq.filename = filepath.Base(grabUrl.Path) upReq.filename = filepath.Base(grabUrl.Path)
upReq.src = http.MaxBytesReader(w, resp.Body, Config.maxSize) upReq.src = http.MaxBytesReader(w, resp.Body, Config.maxSize)
upReq.deleteKey = r.FormValue("deletekey") upReq.deleteKey = r.FormValue("deletekey")
upReq.accessKey = r.FormValue(accessKeyParamName)
upReq.randomBarename = r.FormValue("randomize") == "yes" upReq.randomBarename = r.FormValue("randomize") == "yes"
upReq.expiry = parseExpiry(r.FormValue("expiry")) upReq.expiry = parseExpiry(r.FormValue("expiry"))
@ -211,6 +214,7 @@ func uploadHeaderProcess(r *http.Request, upReq *UploadRequest) {
} }
upReq.deleteKey = r.Header.Get("Linx-Delete-Key") upReq.deleteKey = r.Header.Get("Linx-Delete-Key")
upReq.accessKey = r.Header.Get(accessKeyHeaderName)
// Get seconds until expiry. Non-integer responses never expire. // Get seconds until expiry. Non-integer responses never expire.
expStr := r.Header.Get("Linx-Expiry") expStr := r.Header.Get("Linx-Expiry")
@ -310,7 +314,7 @@ func processUpload(upReq UploadRequest) (upload Upload, err error) {
upReq.deleteKey = uniuri.NewLen(30) upReq.deleteKey = uniuri.NewLen(30)
} }
upload.Metadata, err = storageBackend.Put(upload.Filename, io.MultiReader(bytes.NewReader(header), upReq.src), fileExpiry, upReq.deleteKey) upload.Metadata, err = storageBackend.Put(upload.Filename, io.MultiReader(bytes.NewReader(header), upReq.src), fileExpiry, upReq.deleteKey, upReq.accessKey)
if err != nil { if err != nil {
return upload, err return upload, err
} }
@ -328,6 +332,7 @@ func generateJSONresponse(upload Upload, r *http.Request) []byte {
"direct_url": getSiteURL(r) + Config.selifPath + upload.Filename, "direct_url": getSiteURL(r) + Config.selifPath + upload.Filename,
"filename": upload.Filename, "filename": upload.Filename,
"delete_key": upload.Metadata.DeleteKey, "delete_key": upload.Metadata.DeleteKey,
"access_key": upload.Metadata.AccessKey,
"expiry": strconv.FormatInt(upload.Metadata.Expiry.Unix(), 10), "expiry": strconv.FormatInt(upload.Metadata.Expiry.Unix(), 10),
"size": strconv.FormatInt(upload.Metadata.Size, 10), "size": strconv.FormatInt(upload.Metadata.Size, 10),
"mimetype": upload.Metadata.Mimetype, "mimetype": upload.Metadata.Mimetype,