Browse Source

Merge pull request #56 from mutantmonkey/auth

Add support for auth keys (and remote auth keys)
pull/58/head
Andrei Marcu 9 years ago
parent
commit
60239467fd
  1. 2
      README.md
  2. 128
      auth.go
  3. 24
      auth_test.go
  4. 24
      server.go
  5. 41
      server_test.go
  6. 13
      upload.go

2
README.md

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

128
auth.go

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

24
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")
}
}

24
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$`)
if Config.authFile == "" {
mux.Get("/", indexHandler) mux.Get("/", indexHandler)
mux.Get("/paste/", pasteHandler) 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")

41
server_test.go

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

@ -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…
Cancel
Save