Allow Basic authentication in browser (#195)
This commit is contained in:
parent
fabb8f2dd7
commit
597bec430c
|
@ -60,6 +60,7 @@ allowhotlink = true
|
||||||
#### Require API Keys for uploads
|
#### Require API Keys for uploads
|
||||||
- ```-authfile path/to/authfile``` -- (optionally) require authorization for upload/delete by providing a newline-separated file of scrypted auth keys
|
- ```-authfile path/to/authfile``` -- (optionally) require authorization for upload/delete by providing a newline-separated file of scrypted auth keys
|
||||||
- ```-remoteauthfile path/to/remoteauthfile``` -- (optionally) require authorization for remote uploads by providing a newline-separated file of scrypted auth keys
|
- ```-remoteauthfile path/to/remoteauthfile``` -- (optionally) require authorization for remote uploads by providing a newline-separated file of scrypted auth keys
|
||||||
|
- ```-basicauth``` -- (optionally) allow basic authorization to upload or paste files from browser when `-authfile` is enabled
|
||||||
|
|
||||||
A helper utility ```linx-genkey``` is provided which hashes keys to the format required in the auth files.
|
A helper utility ```linx-genkey``` is provided which hashes keys to the format required in the auth files.
|
||||||
|
|
||||||
|
|
21
auth.go
21
auth.go
|
@ -3,11 +3,14 @@ package main
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"golang.org/x/crypto/scrypt"
|
"golang.org/x/crypto/scrypt"
|
||||||
|
|
||||||
|
"github.com/zenazn/goji/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -78,6 +81,12 @@ func (a auth) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
key := r.Header.Get("Linx-Api-Key")
|
key := r.Header.Get("Linx-Api-Key")
|
||||||
|
if key == "" && Config.basicAuth {
|
||||||
|
_, password, ok := r.BasicAuth()
|
||||||
|
if ok {
|
||||||
|
key = password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result, err := checkAuth(a.authKeys, key)
|
result, err := checkAuth(a.authKeys, key)
|
||||||
if err != nil || !result {
|
if err != nil || !result {
|
||||||
|
@ -88,8 +97,8 @@ func (a auth) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
a.successHandler.ServeHTTP(w, r)
|
a.successHandler.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func UploadAuth(o AuthOptions) func(http.Handler) http.Handler {
|
func UploadAuth(o AuthOptions) func(*web.C, http.Handler) http.Handler {
|
||||||
fn := func(h http.Handler) http.Handler {
|
fn := func(c *web.C, h http.Handler) http.Handler {
|
||||||
return auth{
|
return auth{
|
||||||
successHandler: h,
|
successHandler: h,
|
||||||
failureHandler: http.HandlerFunc(badAuthorizationHandler),
|
failureHandler: http.HandlerFunc(badAuthorizationHandler),
|
||||||
|
@ -101,7 +110,13 @@ func UploadAuth(o AuthOptions) func(http.Handler) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
func badAuthorizationHandler(w http.ResponseWriter, r *http.Request) {
|
func badAuthorizationHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
if Config.basicAuth {
|
||||||
|
rs := ""
|
||||||
|
if Config.siteName != "" {
|
||||||
|
rs = fmt.Sprintf(` realm="%s"`, Config.siteName)
|
||||||
|
}
|
||||||
|
w.Header().Set("WWW-Authenticate", `Basic` + rs)
|
||||||
|
}
|
||||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
26
server.go
26
server.go
|
@ -59,6 +59,7 @@ var Config struct {
|
||||||
allowHotlink bool
|
allowHotlink bool
|
||||||
fastcgi bool
|
fastcgi bool
|
||||||
remoteUploads bool
|
remoteUploads bool
|
||||||
|
basicAuth bool
|
||||||
authFile string
|
authFile string
|
||||||
remoteAuthFile string
|
remoteAuthFile string
|
||||||
addHeaders headerList
|
addHeaders headerList
|
||||||
|
@ -168,7 +169,7 @@ func setup() *web.Mux {
|
||||||
selifIndexRe := regexp.MustCompile("^" + Config.sitePath + Config.selifPath + `$`)
|
selifIndexRe := regexp.MustCompile("^" + Config.sitePath + Config.selifPath + `$`)
|
||||||
torrentRe := regexp.MustCompile("^" + Config.sitePath + `(?P<name>[a-z0-9-\.]+)/torrent$`)
|
torrentRe := regexp.MustCompile("^" + Config.sitePath + `(?P<name>[a-z0-9-\.]+)/torrent$`)
|
||||||
|
|
||||||
if Config.authFile == "" {
|
if Config.authFile == "" || Config.basicAuth {
|
||||||
mux.Get(Config.sitePath, indexHandler)
|
mux.Get(Config.sitePath, indexHandler)
|
||||||
mux.Get(Config.sitePath+"paste/", pasteHandler)
|
mux.Get(Config.sitePath+"paste/", pasteHandler)
|
||||||
} else {
|
} else {
|
||||||
|
@ -189,6 +190,27 @@ func setup() *web.Mux {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if Config.basicAuth {
|
||||||
|
options := AuthOptions{
|
||||||
|
AuthFile: Config.authFile,
|
||||||
|
UnauthMethods: []string{},
|
||||||
|
}
|
||||||
|
okFunc := func (w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Location", Config.sitePath)
|
||||||
|
w.WriteHeader(http.StatusFound)
|
||||||
|
}
|
||||||
|
authHandler := auth {
|
||||||
|
successHandler: http.HandlerFunc(okFunc),
|
||||||
|
failureHandler: http.HandlerFunc(badAuthorizationHandler),
|
||||||
|
authKeys: readAuthKeys(Config.authFile),
|
||||||
|
o: options,
|
||||||
|
}
|
||||||
|
mux.Head(Config.sitePath+"auth", authHandler)
|
||||||
|
mux.Head(Config.sitePath+"auth/", authHandler)
|
||||||
|
mux.Get(Config.sitePath+"auth", authHandler)
|
||||||
|
mux.Get(Config.sitePath+"auth/", authHandler)
|
||||||
|
}
|
||||||
|
|
||||||
mux.Post(Config.sitePath+"upload", uploadPostHandler)
|
mux.Post(Config.sitePath+"upload", uploadPostHandler)
|
||||||
mux.Post(Config.sitePath+"upload/", uploadPostHandler)
|
mux.Post(Config.sitePath+"upload/", uploadPostHandler)
|
||||||
mux.Put(Config.sitePath+"upload", uploadPutHandler)
|
mux.Put(Config.sitePath+"upload", uploadPutHandler)
|
||||||
|
@ -217,6 +239,8 @@ func main() {
|
||||||
"path to files directory")
|
"path to files directory")
|
||||||
flag.StringVar(&Config.metaDir, "metapath", "meta/",
|
flag.StringVar(&Config.metaDir, "metapath", "meta/",
|
||||||
"path to metadata directory")
|
"path to metadata directory")
|
||||||
|
flag.BoolVar(&Config.basicAuth, "basicauth", false,
|
||||||
|
"allow logging by basic auth password")
|
||||||
flag.BoolVar(&Config.noLogs, "nologs", false,
|
flag.BoolVar(&Config.noLogs, "nologs", false,
|
||||||
"remove stdout output for each request")
|
"remove stdout output for each request")
|
||||||
flag.BoolVar(&Config.allowHotlink, "allowhotlink", false,
|
flag.BoolVar(&Config.allowHotlink, "allowhotlink", false,
|
||||||
|
|
|
@ -6,6 +6,23 @@ Dropzone.options.dropzone = {
|
||||||
dzone.style.display = "block";
|
dzone.style.display = "block";
|
||||||
},
|
},
|
||||||
addedfile: function(file) {
|
addedfile: function(file) {
|
||||||
|
if (!this.options.autoProcessQueue) {
|
||||||
|
var dropzone = this;
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.onload = function() {
|
||||||
|
if (xhr.readyState !== XMLHttpRequest.DONE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (xhr.status < 400) {
|
||||||
|
dropzone.processQueue()
|
||||||
|
dropzone.options.autoProcessQueue = true;
|
||||||
|
} else {
|
||||||
|
dropzone.cancelUpload(file)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.open("HEAD", "auth/", true);
|
||||||
|
xhr.send()
|
||||||
|
}
|
||||||
var upload = document.createElement("div");
|
var upload = document.createElement("div");
|
||||||
upload.className = "upload";
|
upload.className = "upload";
|
||||||
|
|
||||||
|
@ -80,6 +97,9 @@ Dropzone.options.dropzone = {
|
||||||
file.cancelActionElement = deleteAction;
|
file.cancelActionElement = deleteAction;
|
||||||
file.fileActions.appendChild(deleteAction);
|
file.fileActions.appendChild(deleteAction);
|
||||||
},
|
},
|
||||||
|
canceled: function(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);
|
||||||
|
@ -101,6 +121,7 @@ Dropzone.options.dropzone = {
|
||||||
file.fileLabel.className = "error";
|
file.fileLabel.className = "error";
|
||||||
},
|
},
|
||||||
|
|
||||||
|
autoProcessQueue: document.getElementById("dropzone").getAttribute("data-auth") !== "basic",
|
||||||
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,
|
||||||
|
|
10
templates.go
10
templates.go
|
@ -84,7 +84,15 @@ func renderTemplate(tpl *pongo2.Template, context pongo2.Context, r *http.Reques
|
||||||
|
|
||||||
context["sitepath"] = Config.sitePath
|
context["sitepath"] = Config.sitePath
|
||||||
context["selifpath"] = Config.selifPath
|
context["selifpath"] = Config.selifPath
|
||||||
context["using_auth"] = Config.authFile != ""
|
var a string
|
||||||
|
if Config.authFile == "" {
|
||||||
|
a = "none"
|
||||||
|
} else if Config.basicAuth {
|
||||||
|
a = "basic"
|
||||||
|
} else {
|
||||||
|
a = "header"
|
||||||
|
}
|
||||||
|
context["auth"] = a
|
||||||
|
|
||||||
return tpl.ExecuteWriter(context, writer)
|
return tpl.ExecuteWriter(context, writer)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<h3>Client</h3>
|
<h3>Client</h3>
|
||||||
<p>To simplify uploading and deleting files, you can use <a target="_blank" href="https://github.com/andreimarcu/linx-client">linx-client</a>, which uses this API.</p>
|
<p>To simplify uploading and deleting files, you can use <a target="_blank" href="https://github.com/andreimarcu/linx-client">linx-client</a>, which uses this API.</p>
|
||||||
|
|
||||||
{% if using_auth %}
|
{% if auth != "none" %}
|
||||||
<h3>Keys</h3>
|
<h3>Keys</h3>
|
||||||
<p>This instance uses API Keys, therefore you will need to provide a key for uploading and deleting files.<br/> To do so, add the <code>Linx-Api-Key</code> header with your key.</p>
|
<p>This instance uses API Keys, therefore you will need to provide a key for uploading and deleting files.<br/> To do so, add the <code>Linx-Api-Key</code> header with your key.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -56,7 +56,7 @@
|
||||||
|
|
||||||
<p>Uploading myphoto.jpg</p>
|
<p>Uploading myphoto.jpg</p>
|
||||||
|
|
||||||
{% if using_auth %}
|
{% if auth != "none" %}
|
||||||
<pre><code>$ curl -H "Linx-Api-Key: mysecretkey" -T myphoto.jpg {{ siteurl }}upload/
|
<pre><code>$ curl -H "Linx-Api-Key: mysecretkey" -T myphoto.jpg {{ siteurl }}upload/
|
||||||
{{ siteurl }}{% if not forcerandom %}myphoto.jpg{% else %}7z4h4ut.jpg{% endif %}</code></pre>
|
{{ siteurl }}{% if not forcerandom %}myphoto.jpg{% else %}7z4h4ut.jpg{% endif %}</code></pre>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -66,7 +66,7 @@
|
||||||
|
|
||||||
<p>Uploading myphoto.jpg with an expiry of 20 minutes</p>
|
<p>Uploading myphoto.jpg with an expiry of 20 minutes</p>
|
||||||
|
|
||||||
{% if using_auth %}
|
{% if auth != "none" %}
|
||||||
<pre><code>$ curl -H "Linx-Api-Key: mysecretkey" -H "Linx-Expiry: 1200" -T myphoto.jpg {{ siteurl }}upload/
|
<pre><code>$ curl -H "Linx-Api-Key: mysecretkey" -H "Linx-Expiry: 1200" -T myphoto.jpg {{ siteurl }}upload/
|
||||||
{{ siteurl }}{% if not forcerandom %}myphoto.jpg{% else %}jm295snf.jpg{% endif %}</code></pre>
|
{{ siteurl }}{% if not forcerandom %}myphoto.jpg{% else %}jm295snf.jpg{% endif %}</code></pre>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -76,7 +76,7 @@
|
||||||
|
|
||||||
<p>Uploading myphoto.jpg with a random filename and getting a json response:</p>
|
<p>Uploading myphoto.jpg with a random filename and getting a json response:</p>
|
||||||
|
|
||||||
{% if using_auth %}
|
{% if auth != "none" %}
|
||||||
<pre><code>$ curl -H "Linx-Api-Key: mysecretkey" -H "Accept: application/json"{% if not forcerandom %} -H "Linx-Randomize: yes"{% endif %} -T myphoto.jpg {{ siteurl }}upload/
|
<pre><code>$ curl -H "Linx-Api-Key: mysecretkey" -H "Accept: application/json"{% if not forcerandom %} -H "Linx-Randomize: yes"{% endif %} -T myphoto.jpg {{ siteurl }}upload/
|
||||||
{"delete_key":"...","expiry":"0","filename":"f34h4iu.jpg","mimetype":"image/jpeg",
|
{"delete_key":"...","expiry":"0","filename":"f34h4iu.jpg","mimetype":"image/jpeg",
|
||||||
"sha256sum":"...","size":"...","url":"{{ siteurl }}f34h4iu.jpg"}</code></pre>
|
"sha256sum":"...","size":"...","url":"{{ siteurl }}f34h4iu.jpg"}</code></pre>
|
||||||
|
@ -94,7 +94,7 @@
|
||||||
|
|
||||||
<p>To overwrite myphoto.jpg</p>
|
<p>To overwrite myphoto.jpg</p>
|
||||||
|
|
||||||
{% if using_auth %}
|
{% if auth != "none" %}
|
||||||
<pre><code>$ curl -H "Linx-Api-Key: mysecretkey" -H "Linx-Delete-Key: mysecret" -T myphoto.jpg {{ siteurl }}upload/
|
<pre><code>$ curl -H "Linx-Api-Key: mysecretkey" -H "Linx-Delete-Key: mysecret" -T myphoto.jpg {{ siteurl }}upload/
|
||||||
{{ siteurl }}myphoto.jpg</code></pre>
|
{{ siteurl }}myphoto.jpg</code></pre>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -110,7 +110,7 @@
|
||||||
|
|
||||||
<p>To delete myphoto.jpg</p>
|
<p>To delete myphoto.jpg</p>
|
||||||
|
|
||||||
{% if using_auth %}
|
{% if auth != "none" %}
|
||||||
<pre><code>$ curl -H "Linx-Api-Key: mysecretkey" -H "Linx-Delete-Key: mysecret" -X DELETE {{ siteurl }}myphoto.jpg
|
<pre><code>$ curl -H "Linx-Api-Key: mysecretkey" -H "Linx-Delete-Key: mysecret" -X DELETE {{ siteurl }}myphoto.jpg
|
||||||
DELETED</code></pre>
|
DELETED</code></pre>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<div id="container">
|
<div id="container">
|
||||||
<div id="header">
|
<div id="header">
|
||||||
<div id="navigation" class="right">
|
<div id="navigation" class="right">
|
||||||
{% if !using_auth %}
|
{% if auth != "header" %}
|
||||||
<a href="{{ sitepath }}">Upload</a> |
|
<a href="{{ sitepath }}">Upload</a> |
|
||||||
<a href="{{ sitepath }}paste/">Paste</a> |
|
<a href="{{ sitepath }}paste/">Paste</a> |
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="fileupload">
|
<div id="fileupload">
|
||||||
<form action="{{ sitepath }}upload" class="dropzone" id="dropzone" method="POST" enctype="multipart/form-data" data-maxsize="{{ maxsize }}">
|
<form action="{{ sitepath }}upload" class="dropzone" id="dropzone" method="POST" enctype="multipart/form-data" data-maxsize="{{ maxsize }}" data-auth="{{ auth }}">
|
||||||
<div class="fallback">
|
<div class="fallback">
|
||||||
<input id="fileinput" name="file" type="file" /><br />
|
<input id="fileinput" name="file" type="file" /><br />
|
||||||
<input id="submitbtn" type="submit" value="Upload">
|
<input id="submitbtn" type="submit" value="Upload">
|
||||||
|
|
13
upload.go
13
upload.go
|
@ -157,9 +157,20 @@ func uploadPutHandler(c web.C, w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func uploadRemote(c web.C, w http.ResponseWriter, r *http.Request) {
|
func uploadRemote(c web.C, w http.ResponseWriter, r *http.Request) {
|
||||||
if Config.remoteAuthFile != "" {
|
if Config.remoteAuthFile != "" {
|
||||||
result, err := checkAuth(remoteAuthKeys, r.FormValue("key"))
|
key := r.FormValue("key")
|
||||||
|
if key == "" && Config.basicAuth {
|
||||||
|
_, password, ok := r.BasicAuth()
|
||||||
|
if ok {
|
||||||
|
key = password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result, err := checkAuth(remoteAuthKeys, key)
|
||||||
if err != nil || !result {
|
if err != nil || !result {
|
||||||
|
if Config.basicAuth {
|
||||||
|
badAuthorizationHandler(w, r)
|
||||||
|
} else {
|
||||||
unauthorizedHandler(c, w, r)
|
unauthorizedHandler(c, w, r)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue