add support for auth keys
Add a middleware that requires authorization for all POST, PUT, and DELETE requests. This is done using the Authorization header and the provided auth key is then checked against a file containing scrypted auth keys. These keys are salted the constant string `linx-server`.
This commit is contained in:
parent
2b0135697b
commit
aa7dad3a03
|
@ -30,6 +30,7 @@ Command-line options
|
|||
- ```-xframeoptions "..." ``` -- X-Frame-Options header (default is "SAMEORIGIN")
|
||||
- ```-remoteuploads``` -- (optionally) enable remote uploads (/upload?url=https://...)
|
||||
- ```-realip``` -- (optionally) let linx-server know you (nginx, etc) are providing the X-Real-IP and/or X-Forwarded-For headers.
|
||||
- ````-authfile``` -- (optionally) require authorization for upload/delete by providing a newline-separated file of scrypted auth keys
|
||||
- ```-fastcgi``` -- (optionally) serve through fastcgi
|
||||
- ```-nologs``` -- (optionally) disable request logs in stdout
|
||||
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/scrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
authPrefix = "Linx "
|
||||
scryptSalt = "linx-server"
|
||||
scryptN = 16384
|
||||
scryptr = 8
|
||||
scryptp = 1
|
||||
scryptKeyLen = 32
|
||||
)
|
||||
|
||||
type AuthOptions struct {
|
||||
AuthFile string
|
||||
UnauthMethods []string
|
||||
}
|
||||
|
||||
type uploadBasicAuth struct {
|
||||
successHandler http.Handler
|
||||
failureHandler http.Handler
|
||||
o AuthOptions
|
||||
}
|
||||
|
||||
func checkAuth(authFile string, decodedAuth []byte) (result bool, err error) {
|
||||
f, err := os.Open(authFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
checkKey, err := scrypt.Key([]byte(decodedAuth), []byte(scryptSalt), scryptN, scryptr, scryptp, scryptKeyLen)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
encodedKey := base64.StdEncoding.EncodeToString(checkKey)
|
||||
|
||||
scanner := bufio.NewScanner(bufio.NewReader(f))
|
||||
for scanner.Scan() {
|
||||
if encodedKey == scanner.Text() {
|
||||
result = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result = false
|
||||
err = scanner.Err()
|
||||
return
|
||||
}
|
||||
|
||||
func (a uploadBasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if sliceContains(a.o.UnauthMethods, r.Method) {
|
||||
// allow unauthenticated methods
|
||||
a.successHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(authHeader, authPrefix) {
|
||||
a.failureHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
decodedAuth, err := base64.StdEncoding.DecodeString(authHeader[len(authPrefix):])
|
||||
if err != nil {
|
||||
a.failureHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := checkAuth(a.o.AuthFile, decodedAuth)
|
||||
if err != nil || !result {
|
||||
a.failureHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
a.successHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func UploadAuth(o AuthOptions) func(http.Handler) http.Handler {
|
||||
fn := func(h http.Handler) http.Handler {
|
||||
return uploadBasicAuth{
|
||||
successHandler: h,
|
||||
failureHandler: http.HandlerFunc(badAuthorizationHandler),
|
||||
o: o,
|
||||
}
|
||||
}
|
||||
return fn
|
||||
}
|
||||
|
||||
func badAuthorizationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func sliceContains(slice []string, s string) bool {
|
||||
for _, v := range slice {
|
||||
if s == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
20
server.go
20
server.go
|
@ -35,6 +35,7 @@ var Config struct {
|
|||
allowHotlink bool
|
||||
fastcgi bool
|
||||
remoteUploads bool
|
||||
authFile string
|
||||
}
|
||||
|
||||
var Templates = make(map[string]*pongo2.Template)
|
||||
|
@ -64,6 +65,13 @@ func setup() *web.Mux {
|
|||
frame: Config.xFrameOptions,
|
||||
}))
|
||||
|
||||
if Config.authFile != "" {
|
||||
mux.Use(UploadAuth(AuthOptions{
|
||||
AuthFile: Config.authFile,
|
||||
UnauthMethods: []string{"GET", "HEAD", "OPTIONS", "TRACE"},
|
||||
}))
|
||||
}
|
||||
|
||||
// make directories if needed
|
||||
err := os.MkdirAll(Config.filesDir, 0755)
|
||||
if err != nil {
|
||||
|
@ -103,9 +111,15 @@ func setup() *web.Mux {
|
|||
selifIndexRe := regexp.MustCompile(`^/selif/$`)
|
||||
torrentRe := regexp.MustCompile(`^/(?P<name>[a-z0-9-\.]+)/torrent$`)
|
||||
|
||||
mux.Get("/", indexHandler)
|
||||
mux.Get("/paste/", pasteHandler)
|
||||
if Config.authFile == "" {
|
||||
mux.Get("/", indexHandler)
|
||||
mux.Get("/paste/", pasteHandler)
|
||||
} else {
|
||||
mux.Get("/", http.RedirectHandler("/API", 303))
|
||||
mux.Get("/paste/", http.RedirectHandler("/API/", 303))
|
||||
}
|
||||
mux.Get("/paste", http.RedirectHandler("/paste/", 301))
|
||||
|
||||
mux.Get("/API/", apiDocHandler)
|
||||
mux.Get("/API", http.RedirectHandler("/API/", 301))
|
||||
|
||||
|
@ -159,6 +173,8 @@ func main() {
|
|||
"serve through fastcgi")
|
||||
flag.BoolVar(&Config.remoteUploads, "remoteuploads", false,
|
||||
"enable remote uploads")
|
||||
flag.StringVar(&Config.authFile, "authfile", "",
|
||||
"path to a file containing newline-separated scrypted auth keys")
|
||||
flag.StringVar(&Config.contentSecurityPolicy, "contentsecuritypolicy",
|
||||
"default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; referrer none;",
|
||||
"value of default Content-Security-Policy header")
|
||||
|
|
|
@ -52,6 +52,26 @@ func TestIndex(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIndexAuthKeys(t *testing.T) {
|
||||
Config.authFile = "/dev/null"
|
||||
|
||||
mux := setup()
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mux.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != 303 {
|
||||
t.Fatalf("Status code is not 301, but %d", w.Code)
|
||||
}
|
||||
|
||||
Config.authFile = ""
|
||||
}
|
||||
|
||||
func TestNotFound(t *testing.T) {
|
||||
mux := setup()
|
||||
w := httptest.NewRecorder()
|
||||
|
|
Loading…
Reference in New Issue