Merge branch 'stek29-accesskey'
This commit is contained in:
commit
e4468715ac
|
@ -15,7 +15,7 @@ You can see what it looks like using the demo: [https://demo.linx-server.net/](h
|
||||||
- Display syntax-highlighted code with in-place editing
|
- Display syntax-highlighted code with in-place editing
|
||||||
- Documented API with keys if need to restrict uploads (can use [linx-client](https://github.com/andreimarcu/linx-client) for uploading through command-line)
|
- Documented API with keys if need to restrict uploads (can use [linx-client](https://github.com/andreimarcu/linx-client) for uploading through command-line)
|
||||||
- Torrent download of files using web seeding
|
- Torrent download of files using web seeding
|
||||||
- File expiry, deletion key, and random filename options
|
- File expiry, deletion key, file access key, and random filename options
|
||||||
|
|
||||||
|
|
||||||
### Screenshots
|
### Screenshots
|
||||||
|
|
|
@ -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 {
|
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
|
||||||
|
@ -84,12 +86,13 @@ func (b LocalfsBackend) writeMetadata(key string, metadata backends.Metadata) er
|
||||||
metaPath := path.Join(b.metaPath, key)
|
metaPath := path.Join(b.metaPath, key)
|
||||||
|
|
||||||
mjson := MetadataJSON{
|
mjson := MetadataJSON{
|
||||||
DeleteKey: metadata.DeleteKey,
|
DeleteKey: metadata.DeleteKey,
|
||||||
Mimetype: metadata.Mimetype,
|
AccessKey: metadata.AccessKey,
|
||||||
|
Mimetype: metadata.Mimetype,
|
||||||
ArchiveFiles: metadata.ArchiveFiles,
|
ArchiveFiles: metadata.ArchiveFiles,
|
||||||
Sha256sum: metadata.Sha256sum,
|
Sha256sum: metadata.Sha256sum,
|
||||||
Expiry: metadata.Expiry.Unix(),
|
Expiry: metadata.Expiry.Unix(),
|
||||||
Size: metadata.Size,
|
Size: metadata.Size,
|
||||||
}
|
}
|
||||||
|
|
||||||
dst, err := os.Create(metaPath)
|
dst, err := os.Create(metaPath)
|
||||||
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,10 +109,15 @@ func unmapMetadata(input map[string]*string) (m backends.Metadata, err error) {
|
||||||
|
|
||||||
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
|
||||||
|
@ -137,6 +143,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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
22
display.go
22
display.go
|
@ -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,
|
||||||
|
|
10
fileserve.go
10
fileserve.go
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
|
@ -69,6 +69,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)
|
||||||
|
@ -222,7 +223,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)
|
||||||
|
@ -297,6 +299,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()
|
||||||
|
|
||||||
|
|
|
@ -264,6 +264,31 @@ body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#access_key {
|
||||||
|
min-width: 100%;
|
||||||
|
line-height: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#access_key input, span {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#access_key_checkbox {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#access_key_checkbox:checked ~ #access_key_input {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
#access_key_checkbox:checked ~ #access_key_text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#access_key_input {
|
||||||
|
padding: 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.oopscontent {
|
.oopscontent {
|
||||||
width: 400px;
|
width: 400px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
||||||
|
|
||||||
Dropzone.options.dropzone = {
|
Dropzone.options.dropzone = {
|
||||||
init: function() {
|
init: function () {
|
||||||
var dzone = document.getElementById("dzone");
|
var dzone = document.getElementById("dzone");
|
||||||
dzone.style.display = "block";
|
dzone.style.display = "block";
|
||||||
},
|
},
|
||||||
addedfile: function(file) {
|
addedfile: function (file) {
|
||||||
if (!this.options.autoProcessQueue) {
|
if (!this.options.autoProcessQueue) {
|
||||||
var dropzone = this;
|
var dropzone = this;
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.onload = function() {
|
xhr.onload = function () {
|
||||||
if (xhr.readyState !== XMLHttpRequest.DONE) {
|
if (xhr.readyState !== XMLHttpRequest.DONE) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ Dropzone.options.dropzone = {
|
||||||
var cancelAction = document.createElement("span");
|
var cancelAction = document.createElement("span");
|
||||||
cancelAction.className = "cancel";
|
cancelAction.className = "cancel";
|
||||||
cancelAction.innerHTML = "Cancel";
|
cancelAction.innerHTML = "Cancel";
|
||||||
cancelAction.addEventListener('click', function(ev) {
|
cancelAction.addEventListener('click', function (ev) {
|
||||||
this.removeFile(file);
|
this.removeFile(file);
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
file.cancelActionElement = cancelAction;
|
file.cancelActionElement = cancelAction;
|
||||||
|
@ -53,19 +53,19 @@ Dropzone.options.dropzone = {
|
||||||
|
|
||||||
document.getElementById("uploads").appendChild(upload);
|
document.getElementById("uploads").appendChild(upload);
|
||||||
},
|
},
|
||||||
uploadprogress: function(file, p, bytesSent) {
|
uploadprogress: function (file, p, bytesSent) {
|
||||||
p = parseInt(p);
|
p = parseInt(p);
|
||||||
file.progressElement.innerHTML = p + "%";
|
file.progressElement.innerHTML = p + "%";
|
||||||
file.uploadElement.setAttribute("style", 'background-image: -webkit-linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%); background-image: -moz-linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%); background-image: -ms-linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%); background-image: -o-linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%); background-image: linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%)');
|
file.uploadElement.setAttribute("style", 'background-image: -webkit-linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%); background-image: -moz-linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%); background-image: -ms-linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%); background-image: -o-linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%); background-image: linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%)');
|
||||||
},
|
},
|
||||||
sending: function(file, xhr, formData) {
|
sending: function (file, xhr, formData) {
|
||||||
var randomize = document.getElementById("randomize");
|
var randomize = document.getElementById("randomize");
|
||||||
if(randomize != null) {
|
if (randomize != null) {
|
||||||
formData.append("randomize", randomize.checked);
|
formData.append("randomize", randomize.checked);
|
||||||
}
|
}
|
||||||
formData.append("expires", document.getElementById("expires").value);
|
formData.append("expires", document.getElementById("expires").value);
|
||||||
},
|
},
|
||||||
success: function(file, resp) {
|
success: function (file, resp) {
|
||||||
file.fileActions.removeChild(file.progressElement);
|
file.fileActions.removeChild(file.progressElement);
|
||||||
|
|
||||||
var fileLabelLink = document.createElement("a");
|
var fileLabelLink = document.createElement("a");
|
||||||
|
@ -79,11 +79,11 @@ Dropzone.options.dropzone = {
|
||||||
var deleteAction = document.createElement("span");
|
var deleteAction = document.createElement("span");
|
||||||
deleteAction.innerHTML = "Delete";
|
deleteAction.innerHTML = "Delete";
|
||||||
deleteAction.className = "cancel";
|
deleteAction.className = "cancel";
|
||||||
deleteAction.addEventListener('click', function(ev) {
|
deleteAction.addEventListener('click', function (ev) {
|
||||||
xhr = new XMLHttpRequest();
|
xhr = new XMLHttpRequest();
|
||||||
xhr.open("DELETE", resp.url, true);
|
xhr.open("DELETE", resp.url, true);
|
||||||
xhr.setRequestHeader("Linx-Delete-Key", resp.delete_key);
|
xhr.setRequestHeader("Linx-Delete-Key", resp.delete_key);
|
||||||
xhr.onreadystatechange = function(file) {
|
xhr.onreadystatechange = function (file) {
|
||||||
if (xhr.readyState == 4 && xhr.status === 200) {
|
if (xhr.readyState == 4 && xhr.status === 200) {
|
||||||
var text = document.createTextNode("Deleted ");
|
var text = document.createTextNode("Deleted ");
|
||||||
file.fileLabel.insertBefore(text, file.fileLabelLink);
|
file.fileLabel.insertBefore(text, file.fileLabelLink);
|
||||||
|
@ -97,15 +97,15 @@ Dropzone.options.dropzone = {
|
||||||
file.cancelActionElement = deleteAction;
|
file.cancelActionElement = deleteAction;
|
||||||
file.fileActions.appendChild(deleteAction);
|
file.fileActions.appendChild(deleteAction);
|
||||||
},
|
},
|
||||||
canceled: function(file) {
|
canceled: function (file) {
|
||||||
this.options.error(file);
|
this.options.error(file);
|
||||||
},
|
},
|
||||||
error: function(file, resp, xhrO) {
|
error: function (file, resp, xhrO) {
|
||||||
file.fileActions.removeChild(file.cancelActionElement);
|
file.fileActions.removeChild(file.cancelActionElement);
|
||||||
file.fileActions.removeChild(file.progressElement);
|
file.fileActions.removeChild(file.progressElement);
|
||||||
|
|
||||||
if (file.status === "canceled") {
|
if (file.status === "canceled") {
|
||||||
file.fileLabel.innerHTML = file.name + ": Canceled ";
|
file.fileLabel.innerHTML = file.name + ": Canceled ";
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (resp.error) {
|
if (resp.error) {
|
||||||
|
@ -125,12 +125,12 @@ Dropzone.options.dropzone = {
|
||||||
maxFilesize: Math.round(parseInt(document.getElementById("dropzone").getAttribute("data-maxsize"), 10) / 1024 / 1024),
|
maxFilesize: Math.round(parseInt(document.getElementById("dropzone").getAttribute("data-maxsize"), 10) / 1024 / 1024),
|
||||||
previewsContainer: "#uploads",
|
previewsContainer: "#uploads",
|
||||||
parallelUploads: 5,
|
parallelUploads: 5,
|
||||||
headers: {"Accept": "application/json"},
|
headers: { "Accept": "application/json" },
|
||||||
dictDefaultMessage: "Click or Drop file(s) or Paste image",
|
dictDefaultMessage: "Click or Drop file(s) or Paste image",
|
||||||
dictFallbackMessage: ""
|
dictFallbackMessage: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
document.onpaste = function(event) {
|
document.onpaste = function (event) {
|
||||||
var items = (event.clipboardData || event.originalEvent.clipboardData).items;
|
var items = (event.clipboardData || event.originalEvent.clipboardData).items;
|
||||||
for (index in items) {
|
for (index in items) {
|
||||||
var item = items[index];
|
var item = items[index];
|
||||||
|
@ -140,4 +140,10 @@ document.onpaste = function(event) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
document.getElementById("access_key_checkbox").onchange = function (event) {
|
||||||
|
if (event.target.checked == false) {
|
||||||
|
document.getElementById("access_key_input").value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// @end-license
|
// @end-license
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -33,6 +33,9 @@
|
||||||
<p>Specify a custom deletion key<br/>
|
<p>Specify a custom deletion key<br/>
|
||||||
<code>Linx-Delete-Key: mysecret</code></p>
|
<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/>
|
<p>Specify an expiration time (in seconds)<br/>
|
||||||
<code>Linx-Expiry: 60</code></p>
|
<code>Linx-Expiry: 60</code></p>
|
||||||
|
|
||||||
|
@ -46,6 +49,7 @@
|
||||||
“direct_url”: the url to access the file directly<br/>
|
“direct_url”: the url to access the file directly<br/>
|
||||||
“filename”: the (optionally generated) filename<br/>
|
“filename”: the (optionally generated) filename<br/>
|
||||||
“delete_key”: the (optionally generated) deletion key,<br/>
|
“delete_key”: the (optionally generated) deletion key,<br/>
|
||||||
|
“access_key”: the (optionally supplied) access key,<br/>
|
||||||
“expiry”: the unix timestamp at which the file will expire (0 if never)<br/>
|
“expiry”: the unix timestamp at which the file will expire (0 if never)<br/>
|
||||||
“size”: the size in bytes of the file<br/>
|
“size”: the size in bytes of the file<br/>
|
||||||
“mimetype”: the guessed mimetype of the file<br/>
|
“mimetype”: the guessed mimetype of the file<br/>
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div id="main" class="oopscontent">
|
||||||
|
<form action="{{ unlockpath }}" method="POST" enctype="multipart/form-data">
|
||||||
|
{{ filename }} is protected with a password: <br /><br />
|
||||||
|
<input name="access_key" type="password" />
|
||||||
|
<input id="submitbtn" type="submit" value="Unlock">
|
||||||
|
<br /><br />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -17,16 +17,28 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="choices">
|
<div id="choices">
|
||||||
<label>{% if not forcerandom %}<input name="randomize" id="randomize" type="checkbox" checked /> Randomize filename{% endif %}</label>
|
<span class="hint--top hint--bounce" data-hint="Replace the filename with random characters. The file extension is retained">
|
||||||
|
<label><input {% if forcerandom %} disabled {% endif %} name="randomize" id="randomize" type="checkbox" checked /> Randomize filename</label>
|
||||||
|
</span>
|
||||||
|
|
||||||
<div id="expiry">
|
<div id="expiry">
|
||||||
<label>File expiry:
|
<label>File expiry:
|
||||||
<select name="expires" id="expires">
|
<select name="expires" id="expires">
|
||||||
{% for expiry in expirylist %}
|
{% for expiry in expirylist %}
|
||||||
<option value="{{ expiry.Seconds }}"{% if forloop.Last %} selected{% endif %}>{{ expiry.Human }}</option>
|
<option value="{{ expiry.Seconds }}"{% if forloop.Last %} selected{% endif %}>{{ expiry.Human }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="access_key">
|
||||||
|
<span class="hint--top hint--bounce" data-hint="Require password to access (this does not encrypt the file but only limits access)">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="access_key_checkbox"/>
|
||||||
|
<input id="access_key_input" name="access_key" type="text" placeholder="Access password"/>
|
||||||
|
<span id="access_key_text">Require access password</span>
|
||||||
|
</label>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="clear"></div>
|
<div class="clear"></div>
|
||||||
</form>
|
</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>
|
{% 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>
|
||||||
<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">
|
<select id="expiry" name="expires">
|
||||||
<option disabled>Expires:</option>
|
<option disabled>Expires:</option>
|
||||||
{% for expiry in expirylist %}
|
{% for expiry in expirylist %}
|
||||||
|
|
|
@ -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
|
||||||
|
@ -192,6 +194,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"))
|
||||||
|
|
||||||
|
@ -222,6 +225,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")
|
||||||
|
@ -321,7 +325,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
|
||||||
}
|
}
|
||||||
|
@ -339,6 +343,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,
|
||||||
|
|
Loading…
Reference in New Issue