From aa7dad3a03426e4507f9059ec1360a4047edb57a Mon Sep 17 00:00:00 2001 From: mutantmonkey Date: Sat, 10 Oct 2015 12:26:47 -0700 Subject: [PATCH 1/8] 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`. --- README.md | 1 + auth.go | 111 +++++++++++++++++++++++++++++++++++++++++++++++++ server.go | 20 ++++++++- server_test.go | 20 +++++++++ 4 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 auth.go diff --git a/README.md b/README.md index 662403d..7e46836 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/auth.go b/auth.go new file mode 100644 index 0000000..bc7de3e --- /dev/null +++ b/auth.go @@ -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 +} diff --git a/server.go b/server.go index 458ac12..b6e74da 100644 --- a/server.go +++ b/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[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") diff --git a/server_test.go b/server_test.go index 1e382c7..e4c3a3a 100644 --- a/server_test.go +++ b/server_test.go @@ -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() From 3c9e260926732f5bcb464721bada110e29cbe5e7 Mon Sep 17 00:00:00 2001 From: mutantmonkey Date: Sun, 11 Oct 2015 17:43:31 -0700 Subject: [PATCH 2/8] improve auth tests and rename auth struct --- auth.go | 6 +++--- server_test.go | 28 ++++++++++++++++++---------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/auth.go b/auth.go index bc7de3e..688cea3 100644 --- a/auth.go +++ b/auth.go @@ -24,7 +24,7 @@ type AuthOptions struct { UnauthMethods []string } -type uploadBasicAuth struct { +type auth struct { successHandler http.Handler failureHandler http.Handler o AuthOptions @@ -56,7 +56,7 @@ func checkAuth(authFile string, decodedAuth []byte) (result bool, err error) { return } -func (a uploadBasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { +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) @@ -86,7 +86,7 @@ func (a uploadBasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { func UploadAuth(o AuthOptions) func(http.Handler) http.Handler { fn := func(h http.Handler) http.Handler { - return uploadBasicAuth{ + return auth{ successHandler: h, failureHandler: http.HandlerFunc(badAuthorizationHandler), o: o, diff --git a/server_test.go b/server_test.go index e4c3a3a..95c7b57 100644 --- a/server_test.go +++ b/server_test.go @@ -52,21 +52,29 @@ func TestIndex(t *testing.T) { } } -func TestIndexAuthKeys(t *testing.T) { +func TestAuthKeysRedirects(t *testing.T) { Config.authFile = "/dev/null" - mux := setup() - w := httptest.NewRecorder() - - req, err := http.NewRequest("GET", "/", nil) - if err != nil { - t.Fatal(err) + redirects := []string{ + "/", + "/paste/", } - mux.ServeHTTP(w, req) + mux := setup() - if w.Code != 303 { - t.Fatalf("Status code is not 301, but %d", w.Code) + 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) + } } Config.authFile = "" From cc4e2ca0d9fe09081c3930cd1f928a94ee9f6ed9 Mon Sep 17 00:00:00 2001 From: mutantmonkey Date: Sun, 11 Oct 2015 18:36:27 -0700 Subject: [PATCH 3/8] read authfile once only Read the authfile upon initial server start and store the auth keys in the auth struct, rather than reading the file for each page load. --- auth.go | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/auth.go b/auth.go index 688cea3..84b0586 100644 --- a/auth.go +++ b/auth.go @@ -3,6 +3,7 @@ package main import ( "bufio" "encoding/base64" + "log" "net/http" "os" "strings" @@ -27,32 +28,25 @@ type AuthOptions struct { type auth struct { successHandler http.Handler failureHandler http.Handler + authKeys []string o AuthOptions } -func checkAuth(authFile string, decodedAuth []byte) (result bool, err error) { - f, err := os.Open(authFile) - if err != nil { - return - } - +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) - - scanner := bufio.NewScanner(bufio.NewReader(f)) - for scanner.Scan() { - if encodedKey == scanner.Text() { + for _, v := range authKeys { + if encodedKey == v { result = true return } } result = false - err = scanner.Err() return } @@ -75,7 +69,7 @@ func (a auth) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - result, err := checkAuth(a.o.AuthFile, decodedAuth) + result, err := checkAuth(a.authKeys, decodedAuth) if err != nil || !result { a.failureHandler.ServeHTTP(w, r) return @@ -85,10 +79,29 @@ func (a auth) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func UploadAuth(o AuthOptions) func(http.Handler) http.Handler { + var authKeys []string + + f, err := os.Open(o.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) + } + fn := func(h http.Handler) http.Handler { return auth{ successHandler: h, failureHandler: http.HandlerFunc(badAuthorizationHandler), + authKeys: authKeys, o: o, } } From adbc1604dc88116b9f192abecf6566660a215d99 Mon Sep 17 00:00:00 2001 From: mutantmonkey Date: Sun, 11 Oct 2015 18:37:36 -0700 Subject: [PATCH 4/8] add some more auth tests It's going to be difficult to get 100% code coverage, but we can at least ensure that checkAuth works properly. --- auth_test.go | 24 ++++++++++++++++++++++++ server_test.go | 15 ++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 auth_test.go diff --git a/auth_test.go b/auth_test.go new file mode 100644 index 0000000..9cec2ea --- /dev/null +++ b/auth_test.go @@ -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") + } +} diff --git a/server_test.go b/server_test.go index 95c7b57..ebacfc3 100644 --- a/server_test.go +++ b/server_test.go @@ -52,7 +52,7 @@ func TestIndex(t *testing.T) { } } -func TestAuthKeysRedirects(t *testing.T) { +func TestAuthKeys(t *testing.T) { Config.authFile = "/dev/null" redirects := []string{ @@ -77,6 +77,19 @@ func TestAuthKeysRedirects(t *testing.T) { } } + 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 = "" } From 3dc4753b7aea958f4a4dfcde0129cd2d534fc140 Mon Sep 17 00:00:00 2001 From: mutantmonkey Date: Sun, 11 Oct 2015 19:30:01 -0700 Subject: [PATCH 5/8] move reading auth keys into readAuthKeys --- auth.go | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/auth.go b/auth.go index 84b0586..c8de59a 100644 --- a/auth.go +++ b/auth.go @@ -32,6 +32,28 @@ type auth struct { 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 { @@ -79,29 +101,11 @@ func (a auth) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func UploadAuth(o AuthOptions) func(http.Handler) http.Handler { - var authKeys []string - - f, err := os.Open(o.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) - } - fn := func(h http.Handler) http.Handler { return auth{ successHandler: h, failureHandler: http.HandlerFunc(badAuthorizationHandler), - authKeys: authKeys, + authKeys: readAuthKeys(o.AuthFile), o: o, } } From dd4ac3a7ed85823a1cf48beb6907f99ca5482c7e Mon Sep 17 00:00:00 2001 From: mutantmonkey Date: Sun, 11 Oct 2015 19:31:13 -0700 Subject: [PATCH 6/8] add support remote auth keys These are taken as a parameter to the remote upload page. Note that all keys will be logged since this is a GET request. --- server.go | 8 ++++++++ upload.go | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/server.go b/server.go index b6e74da..4cb88e4 100644 --- a/server.go +++ b/server.go @@ -36,6 +36,7 @@ var Config struct { fastcgi bool remoteUploads bool authFile string + remoteAuthFile string } var Templates = make(map[string]*pongo2.Template) @@ -43,6 +44,7 @@ var TemplateSet *pongo2.TemplateSet var staticBox *rice.Box var timeStarted time.Time var timeStartedStr string +var remoteAuthKeys []string func setup() *web.Mux { mux := web.New() @@ -126,6 +128,10 @@ func setup() *web.Mux { if Config.remoteUploads { mux.Get("/upload", uploadRemote) mux.Get("/upload/", uploadRemote) + + if Config.remoteAuthFile != "" { + remoteAuthKeys = readAuthKeys(Config.remoteAuthFile) + } } mux.Post("/upload", uploadPostHandler) @@ -175,6 +181,8 @@ func main() { "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", "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; referrer none;", "value of default Content-Security-Policy header") diff --git a/upload.go b/upload.go index b24a8ce..63582d9 100644 --- a/upload.go +++ b/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) { + 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") == "" { http.Redirect(w, r, "/", 303) return From 2cd432b5d3c56473ffa5cae63588399c93b35759 Mon Sep 17 00:00:00 2001 From: mutantmonkey Date: Sun, 11 Oct 2015 19:33:48 -0700 Subject: [PATCH 7/8] update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7e46836..48f77fe 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Command-line options - ```-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 +- ````-remoteauthfile``` -- (optionally) require authorization for remote uploads by providing a newline-separated file of scrypted auth keys - ```-fastcgi``` -- (optionally) serve through fastcgi - ```-nologs``` -- (optionally) disable request logs in stdout From 613ab24721bd956b7d777fa3fce9f832f730794f Mon Sep 17 00:00:00 2001 From: mutantmonkey Date: Sun, 11 Oct 2015 19:38:04 -0700 Subject: [PATCH 8/8] show usage for -authfile and -remoteauthfile --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 48f77fe..d7e78a3 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,8 @@ 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 -- ````-remoteauthfile``` -- (optionally) require authorization for remote uploads 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 - ```-fastcgi``` -- (optionally) serve through fastcgi - ```-nologs``` -- (optionally) disable request logs in stdout