Merge pull request #56 from mutantmonkey/auth
Add support for auth keys (and remote auth keys)
This commit is contained in:
commit
60239467fd
|
@ -30,6 +30,8 @@ Command-line options
|
||||||
- ```-xframeoptions "..." ``` -- X-Frame-Options header (default is "SAMEORIGIN")
|
- ```-xframeoptions "..." ``` -- X-Frame-Options header (default is "SAMEORIGIN")
|
||||||
- ```-remoteuploads``` -- (optionally) enable remote uploads (/upload?url=https://...)
|
- ```-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.
|
- ```-realip``` -- (optionally) let linx-server know you (nginx, etc) are providing the X-Real-IP and/or X-Forwarded-For headers.
|
||||||
|
- ````-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
|
||||||
- ```-fastcgi``` -- (optionally) serve through fastcgi
|
- ```-fastcgi``` -- (optionally) serve through fastcgi
|
||||||
- ```-nologs``` -- (optionally) disable request logs in stdout
|
- ```-nologs``` -- (optionally) disable request logs in stdout
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/base64"
|
||||||
|
"log"
|
||||||
|
"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 auth struct {
|
||||||
|
successHandler http.Handler
|
||||||
|
failureHandler http.Handler
|
||||||
|
authKeys []string
|
||||||
|
o AuthOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAuthKeys(authFile string) []string {
|
||||||
|
var authKeys []string
|
||||||
|
|
||||||
|
f, err := os.Open(authFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to open authfile: ", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for scanner.Scan() {
|
||||||
|
authKeys = append(authKeys, scanner.Text())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = scanner.Err()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Scanner error while reading authfile: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return authKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkAuth(authKeys []string, decodedAuth []byte) (result bool, err error) {
|
||||||
|
checkKey, err := scrypt.Key([]byte(decodedAuth), []byte(scryptSalt), scryptN, scryptr, scryptp, scryptKeyLen)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedKey := base64.StdEncoding.EncodeToString(checkKey)
|
||||||
|
for _, v := range authKeys {
|
||||||
|
if encodedKey == v {
|
||||||
|
result = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a auth) 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.authKeys, 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 auth{
|
||||||
|
successHandler: h,
|
||||||
|
failureHandler: http.HandlerFunc(badAuthorizationHandler),
|
||||||
|
authKeys: readAuthKeys(o.AuthFile),
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckAuth(t *testing.T) {
|
||||||
|
authKeys := []string{
|
||||||
|
"vhvZ/PT1jeTbTAJ8JdoxddqFtebSxdVb0vwPlYO+4HM=",
|
||||||
|
"vFpNprT9wbHgwAubpvRxYCCpA2FQMAK6hFqPvAGrdZo=",
|
||||||
|
}
|
||||||
|
|
||||||
|
if r, err := checkAuth(authKeys, []byte("")); err != nil && r {
|
||||||
|
t.Fatal("Authorization passed for empty key")
|
||||||
|
}
|
||||||
|
|
||||||
|
if r, err := checkAuth(authKeys, []byte("thisisnotvalid")); err != nil && r {
|
||||||
|
t.Fatal("Authorization passed for invalid key")
|
||||||
|
}
|
||||||
|
|
||||||
|
if r, err := checkAuth(authKeys, []byte("haPVipRnGJ0QovA9nyqK")); err != nil && !r {
|
||||||
|
t.Fatal("Authorization failed for valid key")
|
||||||
|
}
|
||||||
|
}
|
28
server.go
28
server.go
|
@ -35,6 +35,8 @@ var Config struct {
|
||||||
allowHotlink bool
|
allowHotlink bool
|
||||||
fastcgi bool
|
fastcgi bool
|
||||||
remoteUploads bool
|
remoteUploads bool
|
||||||
|
authFile string
|
||||||
|
remoteAuthFile string
|
||||||
}
|
}
|
||||||
|
|
||||||
var Templates = make(map[string]*pongo2.Template)
|
var Templates = make(map[string]*pongo2.Template)
|
||||||
|
@ -42,6 +44,7 @@ var TemplateSet *pongo2.TemplateSet
|
||||||
var staticBox *rice.Box
|
var staticBox *rice.Box
|
||||||
var timeStarted time.Time
|
var timeStarted time.Time
|
||||||
var timeStartedStr string
|
var timeStartedStr string
|
||||||
|
var remoteAuthKeys []string
|
||||||
|
|
||||||
func setup() *web.Mux {
|
func setup() *web.Mux {
|
||||||
mux := web.New()
|
mux := web.New()
|
||||||
|
@ -64,6 +67,13 @@ func setup() *web.Mux {
|
||||||
frame: Config.xFrameOptions,
|
frame: Config.xFrameOptions,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
if Config.authFile != "" {
|
||||||
|
mux.Use(UploadAuth(AuthOptions{
|
||||||
|
AuthFile: Config.authFile,
|
||||||
|
UnauthMethods: []string{"GET", "HEAD", "OPTIONS", "TRACE"},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
// make directories if needed
|
// make directories if needed
|
||||||
err := os.MkdirAll(Config.filesDir, 0755)
|
err := os.MkdirAll(Config.filesDir, 0755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -103,15 +113,25 @@ func setup() *web.Mux {
|
||||||
selifIndexRe := regexp.MustCompile(`^/selif/$`)
|
selifIndexRe := regexp.MustCompile(`^/selif/$`)
|
||||||
torrentRe := regexp.MustCompile(`^/(?P<name>[a-z0-9-\.]+)/torrent$`)
|
torrentRe := regexp.MustCompile(`^/(?P<name>[a-z0-9-\.]+)/torrent$`)
|
||||||
|
|
||||||
mux.Get("/", indexHandler)
|
if Config.authFile == "" {
|
||||||
mux.Get("/paste/", pasteHandler)
|
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("/paste", http.RedirectHandler("/paste/", 301))
|
||||||
|
|
||||||
mux.Get("/API/", apiDocHandler)
|
mux.Get("/API/", apiDocHandler)
|
||||||
mux.Get("/API", http.RedirectHandler("/API/", 301))
|
mux.Get("/API", http.RedirectHandler("/API/", 301))
|
||||||
|
|
||||||
if Config.remoteUploads {
|
if Config.remoteUploads {
|
||||||
mux.Get("/upload", uploadRemote)
|
mux.Get("/upload", uploadRemote)
|
||||||
mux.Get("/upload/", uploadRemote)
|
mux.Get("/upload/", uploadRemote)
|
||||||
|
|
||||||
|
if Config.remoteAuthFile != "" {
|
||||||
|
remoteAuthKeys = readAuthKeys(Config.remoteAuthFile)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mux.Post("/upload", uploadPostHandler)
|
mux.Post("/upload", uploadPostHandler)
|
||||||
|
@ -159,6 +179,10 @@ func main() {
|
||||||
"serve through fastcgi")
|
"serve through fastcgi")
|
||||||
flag.BoolVar(&Config.remoteUploads, "remoteuploads", false,
|
flag.BoolVar(&Config.remoteUploads, "remoteuploads", false,
|
||||||
"enable remote uploads")
|
"enable remote uploads")
|
||||||
|
flag.StringVar(&Config.authFile, "authfile", "",
|
||||||
|
"path to a file containing newline-separated scrypted auth keys")
|
||||||
|
flag.StringVar(&Config.remoteAuthFile, "remoteauthfile", "",
|
||||||
|
"path to a file containing newline-separated scrypted auth keys for remote uploads")
|
||||||
flag.StringVar(&Config.contentSecurityPolicy, "contentsecuritypolicy",
|
flag.StringVar(&Config.contentSecurityPolicy, "contentsecuritypolicy",
|
||||||
"default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; referrer none;",
|
"default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; referrer none;",
|
||||||
"value of default Content-Security-Policy header")
|
"value of default Content-Security-Policy header")
|
||||||
|
|
|
@ -52,6 +52,47 @@ func TestIndex(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAuthKeys(t *testing.T) {
|
||||||
|
Config.authFile = "/dev/null"
|
||||||
|
|
||||||
|
redirects := []string{
|
||||||
|
"/",
|
||||||
|
"/paste/",
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := setup()
|
||||||
|
|
||||||
|
for _, v := range redirects {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", v, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mux.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != 303 {
|
||||||
|
t.Fatalf("Status code is not 303, but %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", "/paste/", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mux.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != 401 {
|
||||||
|
t.Fatalf("Status code is not 401, but %d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
Config.authFile = ""
|
||||||
|
}
|
||||||
|
|
||||||
func TestNotFound(t *testing.T) {
|
func TestNotFound(t *testing.T) {
|
||||||
mux := setup()
|
mux := setup()
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
13
upload.go
13
upload.go
|
@ -138,6 +138,19 @@ 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 != "" {
|
||||||
|
result, err := checkAuth(remoteAuthKeys, []byte(r.FormValue("key")))
|
||||||
|
if err != nil || !result {
|
||||||
|
unauthorizedHandler(c, w, r)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// strict referrer checking is mandatory without remote auth keys
|
||||||
|
if !strictReferrerCheck(r, Config.siteURL, []string{"Linx-Delete-Key", "Linx-Expiry", "Linx-Randomize"}) {
|
||||||
|
badRequestHandler(c, w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if r.FormValue("url") == "" {
|
if r.FormValue("url") == "" {
|
||||||
http.Redirect(w, r, "/", 303)
|
http.Redirect(w, r, "/", 303)
|
||||||
return
|
return
|
||||||
|
|
Loading…
Reference in New Issue