Allow Basic authentication in browser (#195)

This commit is contained in:
Paweł Płazieński 2020-03-07 00:21:49 +01:00 committed by GitHub
parent fabb8f2dd7
commit 597bec430c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 95 additions and 15 deletions

View File

@ -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
View File

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

View File

@ -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,

View File

@ -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,

View File

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

View File

@ -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 &#34;Linx-Api-Key: mysecretkey&#34; -T myphoto.jpg {{ siteurl }}upload/ <pre><code>$ curl -H &#34;Linx-Api-Key: mysecretkey&#34; -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 &#34;Linx-Api-Key: mysecretkey&#34; -H &#34;Linx-Expiry: 1200&#34; -T myphoto.jpg {{ siteurl }}upload/ <pre><code>$ curl -H &#34;Linx-Api-Key: mysecretkey&#34; -H &#34;Linx-Expiry: 1200&#34; -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 &#34;Linx-Api-Key: mysecretkey&#34; -H &#34;Accept: application/json&#34;{% if not forcerandom %} -H &#34;Linx-Randomize: yes&#34;{% endif %} -T myphoto.jpg {{ siteurl }}upload/ <pre><code>$ curl -H &#34;Linx-Api-Key: mysecretkey&#34; -H &#34;Accept: application/json&#34;{% if not forcerandom %} -H &#34;Linx-Randomize: yes&#34;{% endif %} -T myphoto.jpg {{ siteurl }}upload/
{&#34;delete_key&#34;:&#34;...&#34;,&#34;expiry&#34;:&#34;0&#34;,&#34;filename&#34;:&#34;f34h4iu.jpg&#34;,&#34;mimetype&#34;:&#34;image/jpeg&#34;, {&#34;delete_key&#34;:&#34;...&#34;,&#34;expiry&#34;:&#34;0&#34;,&#34;filename&#34;:&#34;f34h4iu.jpg&#34;,&#34;mimetype&#34;:&#34;image/jpeg&#34;,
&#34;sha256sum&#34;:&#34;...&#34;,&#34;size&#34;:&#34;...&#34;,&#34;url&#34;:&#34;{{ siteurl }}f34h4iu.jpg&#34;}</code></pre> &#34;sha256sum&#34;:&#34;...&#34;,&#34;size&#34;:&#34;...&#34;,&#34;url&#34;:&#34;{{ siteurl }}f34h4iu.jpg&#34;}</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 &#34;Linx-Api-Key: mysecretkey&#34; -H &#34;Linx-Delete-Key: mysecret&#34; -T myphoto.jpg {{ siteurl }}upload/ <pre><code>$ curl -H &#34;Linx-Api-Key: mysecretkey&#34; -H &#34;Linx-Delete-Key: mysecret&#34; -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 &#34;Linx-Api-Key: mysecretkey&#34; -H &#34;Linx-Delete-Key: mysecret&#34; -X DELETE {{ siteurl }}myphoto.jpg <pre><code>$ curl -H &#34;Linx-Api-Key: mysecretkey&#34; -H &#34;Linx-Delete-Key: mysecret&#34; -X DELETE {{ siteurl }}myphoto.jpg
DELETED</code></pre> DELETED</code></pre>
{% else %} {% else %}

View File

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

View File

@ -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">

View File

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