Merge branch 'accesskey' of git://github.com/stek29/linx-server into stek29-accesskey
This commit is contained in:
commit
a4240680c8
|
@ -0,0 +1,147 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"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, siteURL, fileName, value string, expires time.Time) {
|
||||
u, err := url.Parse(siteURL)
|
||||
if err != nil {
|
||||
log.Printf("cant parse siteURL (%v): %v", siteURL, err)
|
||||
return
|
||||
}
|
||||
|
||||
cookie := http.Cookie{
|
||||
Name: accessKeyHeaderName,
|
||||
Value: value,
|
||||
HttpOnly: true,
|
||||
Domain: u.Hostname(),
|
||||
Expires: expires,
|
||||
}
|
||||
|
||||
cookie.Path = path.Join(u.Path, fileName)
|
||||
http.SetCookie(w, &cookie)
|
||||
|
||||
cookie.Path = path.Join(u.Path, 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)
|
||||
}
|
|
@ -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"`
|
||||
|
@ -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
|
||||
|
@ -85,6 +87,7 @@ func (b LocalfsBackend) writeMetadata(key string, metadata backends.Metadata) er
|
|||
|
||||
mjson := MetadataJSON{
|
||||
DeleteKey: metadata.DeleteKey,
|
||||
AccessKey: metadata.AccessKey,
|
||||
Mimetype: metadata.Mimetype,
|
||||
ArchiveFiles: metadata.ArchiveFiles,
|
||||
Sha256sum: metadata.Sha256sum,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
type Metadata struct {
|
||||
DeleteKey string
|
||||
AccessKey string
|
||||
Sha256sum string
|
||||
Mimetype string
|
||||
Size int64
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,10 +109,15 @@ func unmapMetadata(input map[string]*string) (m backends.Metadata, err error) {
|
|||
|
||||
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
|
||||
|
@ -137,6 +143,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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
22
display.go
22
display.go
|
@ -5,7 +5,6 @@ import (
|
|||
"io/ioutil"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -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, "", "")
|
||||
|
@ -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,
|
||||
|
|
10
fileserve.go
10
fileserve.go
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
@ -69,6 +69,7 @@ var Config struct {
|
|||
s3Bucket string
|
||||
s3ForcePathStyle bool
|
||||
forceRandomFilename bool
|
||||
accessKeyCookieExpiry uint64
|
||||
}
|
||||
|
||||
var Templates = make(map[string]*pongo2.Template)
|
||||
|
@ -222,7 +223,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)
|
||||
|
@ -297,6 +299,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()
|
||||
|
||||
|
|
|
@ -264,6 +264,24 @@ body {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
#access_key_checkbox {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#access_key_checkbox:checked ~ #access_key {
|
||||
display: inline-block;
|
||||
}
|
||||
#access_key_checkbox:checked ~ #access_key_label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#access_key {
|
||||
line-height: 1em;
|
||||
padding: 1px;
|
||||
border: 3px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.oopscontent {
|
||||
width: 400px;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/GeertJohan/go.rice"
|
||||
rice "github.com/GeertJohan/go.rice"
|
||||
"github.com/flosch/pongo2"
|
||||
)
|
||||
|
||||
|
@ -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",
|
||||
|
|
|
@ -33,6 +33,9 @@
|
|||
<p>Specify a custom deletion key<br/>
|
||||
<code>Linx-Delete-Key: mysecret</code></p>
|
||||
|
||||
<p>Protect file with password<br/>
|
||||
<code>Linx-Access-Key: mysecret</code></p>
|
||||
|
||||
<p>Specify an expiration time (in seconds)<br/>
|
||||
<code>Linx-Expiry: 60</code></p>
|
||||
|
||||
|
@ -46,6 +49,7 @@
|
|||
“direct_url”: the url to access the file directly<br/>
|
||||
“filename”: the (optionally generated) filename<br/>
|
||||
“delete_key”: the (optionally generated) deletion key,<br/>
|
||||
“access_key”: the (optionally generated) access key,<br/>
|
||||
“expiry”: the unix timestamp at which the file will expire (0 if never)<br/>
|
||||
“size”: the size in bytes of the file<br/>
|
||||
“mimetype”: the guessed mimetype of the file<br/>
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<form action="{{ unlockpath }}" method="POST" enctype="multipart/form-data">
|
||||
{{ filename }} is protected with password <br />
|
||||
<input name="access_key" type="password" />
|
||||
<input id="submitbtn" type="submit" value="Unlock">
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -27,6 +27,12 @@
|
|||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input type="checkbox" id="access_key_checkbox"/>
|
||||
<input id="access_key" name="access_key" type="text" placeholder="Access password"/>
|
||||
<label id="access_key_label"> Require password to access</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
</form>
|
||||
|
|
|
@ -8,6 +8,10 @@
|
|||
{% if not forcerandom %}<span class="hint--top hint--bounce" data-hint="Leave empty for random filename"><input class="codebox" name='filename' id="filename" type='text' value="" placeholder="filename" /></span>{% endif %}.<span class="hint--top hint--bounce" data-hint="Enable syntax highlighting by adding the extension"><input id="extension" class="codebox" name='extension' type='text' value="" placeholder="txt" /></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="hint--top hint--bounce" data-hint="Require password to access (leave empty to disable)">
|
||||
<input class="codebox" name="access_key" type="text" placeholder="password"/>
|
||||
</span>
|
||||
|
||||
<select id="expiry" name="expires">
|
||||
<option disabled>Expires:</option>
|
||||
{% for expiry in expirylist %}
|
||||
|
|
|
@ -40,6 +40,7 @@ type UploadRequest struct {
|
|||
expiry time.Duration // Seconds until expiry, 0 = never
|
||||
deleteKey string // Empty string if not defined
|
||||
randomBarename bool
|
||||
accessKey string // Empty string if not defined
|
||||
}
|
||||
|
||||
// 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.accessKey = r.PostFormValue(accessKeyParamName)
|
||||
|
||||
if r.PostFormValue("randomize") == "true" {
|
||||
upReq.randomBarename = true
|
||||
|
@ -192,6 +194,7 @@ func uploadRemote(c web.C, w http.ResponseWriter, r *http.Request) {
|
|||
upReq.filename = filepath.Base(grabUrl.Path)
|
||||
upReq.src = http.MaxBytesReader(w, resp.Body, Config.maxSize)
|
||||
upReq.deleteKey = r.FormValue("deletekey")
|
||||
upReq.accessKey = r.FormValue(accessKeyParamName)
|
||||
upReq.randomBarename = r.FormValue("randomize") == "yes"
|
||||
upReq.expiry = parseExpiry(r.FormValue("expiry"))
|
||||
|
||||
|
@ -222,6 +225,7 @@ func uploadHeaderProcess(r *http.Request, upReq *UploadRequest) {
|
|||
}
|
||||
|
||||
upReq.deleteKey = r.Header.Get("Linx-Delete-Key")
|
||||
upReq.accessKey = r.Header.Get(accessKeyHeaderName)
|
||||
|
||||
// Get seconds until expiry. Non-integer responses never expire.
|
||||
expStr := r.Header.Get("Linx-Expiry")
|
||||
|
@ -321,7 +325,7 @@ func processUpload(upReq UploadRequest) (upload Upload, err error) {
|
|||
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 {
|
||||
return upload, err
|
||||
}
|
||||
|
@ -339,6 +343,7 @@ func generateJSONresponse(upload Upload, r *http.Request) []byte {
|
|||
"direct_url": getSiteURL(r) + Config.selifPath + upload.Filename,
|
||||
"filename": upload.Filename,
|
||||
"delete_key": upload.Metadata.DeleteKey,
|
||||
"access_key": upload.Metadata.AccessKey,
|
||||
"expiry": strconv.FormatInt(upload.Metadata.Expiry.Unix(), 10),
|
||||
"size": strconv.FormatInt(upload.Metadata.Size, 10),
|
||||
"mimetype": upload.Metadata.Mimetype,
|
||||
|
|
Loading…
Reference in New Issue