diff --git a/server/common/api.go b/server/common/api.go new file mode 100644 index 00000000..1000ffbd --- /dev/null +++ b/server/common/api.go @@ -0,0 +1,12 @@ +package common + +import ( + "os" +) + +func IsApiKeyValid(api_key string) bool { + if api_key == os.Getenv("API_KEY") { + return true + } + return false +} diff --git a/server/common/cache.go b/server/common/cache.go index f3d95479..8df88be1 100644 --- a/server/common/cache.go +++ b/server/common/cache.go @@ -85,9 +85,10 @@ func NewKeyValueStore() KeyValueStore { } func (this *KeyValueStore) Get(key string) interface{} { + var val interface{} this.RLock() - defer this.RUnlock() - val := this.cache[key] + val = this.cache[key] + this.RUnlock() return val } diff --git a/server/common/constants.go b/server/common/constants.go index 9dc544f1..b35ab0cb 100644 --- a/server/common/constants.go +++ b/server/common/constants.go @@ -6,21 +6,22 @@ import ( ) const ( - APP_VERSION = "v0.5" - LOG_PATH = "data/state/log/" - CONFIG_PATH = "data/state/config/" - DB_PATH = "data/state/db/" - FTS_PATH = "data/state/search/" - CERT_PATH = "data/state/certs/" - TMP_PATH = "data/cache/tmp/" - COOKIE_NAME_AUTH = "auth" - COOKIE_NAME_PROOF = "proof" - COOKIE_NAME_ADMIN = "admin" - COOKIE_PATH_ADMIN = "/admin/api/" - COOKIE_PATH = "/api/" - FILE_INDEX = "./data/public/index.html" - FILE_ASSETS = "./data/public/" - URL_SETUP = "/admin/setup" + APP_VERSION = "v0.5" + LOG_PATH = "data/state/log/" + CONFIG_PATH = "data/state/config/" + DB_PATH = "data/state/db/" + FTS_PATH = "data/state/search/" + CERT_PATH = "data/state/certs/" + TMP_PATH = "data/cache/tmp/" + COOKIE_NAME_AUTH = "auth" + COOKIE_NAME_PROOF = "proof" + COOKIE_NAME_ADMIN = "admin" + COOKIE_PATH_ADMIN = "/admin/api/" + COOKIE_PATH = "/api/" + FILE_INDEX = "./data/public/index.html" + FILE_ASSETS = "./data/public/" + URL_SETUP = "/admin/setup" + EXPIRATION_API_TOKEN = 3600 ) func init() { diff --git a/server/common/response.go b/server/common/response.go index f74a10af..30309e38 100644 --- a/server/common/response.go +++ b/server/common/response.go @@ -90,6 +90,12 @@ func SendErrorResult(res http.ResponseWriter, err error) { encoder.Encode(APIErrorMessage{"error", m}) } +func SendRaw(res http.ResponseWriter, data interface{}) { + encoder := json.NewEncoder(res) + encoder.SetEscapeHTML(false) + encoder.Encode(data) +} + func Page(stuff string) string { return ` diff --git a/server/ctrl/session.go b/server/ctrl/session.go index ec90d6fa..8e9c5d42 100644 --- a/server/ctrl/session.go +++ b/server/ctrl/session.go @@ -128,6 +128,58 @@ func SessionAuthenticate(ctx *App, res http.ResponseWriter, req *http.Request) { SendSuccessResult(res, nil) } +func SessionAuthenticateExternal(ctx *App, res http.ResponseWriter, req *http.Request) { + api_key := string(req.URL.Query().Get("key")) + if api_key == "" { + SendErrorResult(res, NewError(fmt.Sprintf( + "You need to provide your API key in the request URL (e.g.: '%s?key=foobar'). See https://www.filestash.app/docs/api/#authentication for details, or we can help at support@filestash.app", + req.URL.Path, + ), 403)) + return + } else if IsApiKeyValid(api_key) == false { + SendErrorResult(res, NewError(fmt.Sprintf( + "Invalid API Key provided: %s", + api_key, + ), 401)) + return + } + + ctx.Body["timestamp"] = time.Now().Format(time.RFC3339) + ctx.Body["api_key"] = api_key + session := model.MapStringInterfaceToMapStringString(ctx.Body) + session["path"] = EnforceDirectory(session["path"]) + if _, err := model.NewBackend(ctx, session); err != nil { + Log.Debug("session::auth_external 'NewBackend' %+v", err) + SendErrorResult(res, err) + return + } + s, err := json.Marshal(session) + if err != nil { + Log.Debug("session::auth_external 'Marshal' %+v", err) + SendErrorResult(res, NewError(err.Error(), 500)) + return + } + obfuscate, err := EncryptString(SECRET_KEY_DERIVATE_FOR_USER, string(s)) + if err != nil { + Log.Debug("session::auth_external 'Encryption' %+v", err) + SendErrorResult(res, NewError(err.Error(), 500)) + return + } + + SendRaw(res, struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + Version string `json:"version"` + License string `json:"license"` + ApiDoc string `json:"doc"` + }{ + obfuscate, "bearer", + EXPIRATION_API_TOKEN, "Filestash " + APP_VERSION + "." + BUILD_DATE, + LICENSE, "https://www.filestash.app/docs/api/", + }) +} + func SessionLogout(ctx *App, res http.ResponseWriter, req *http.Request) { go func() { // user typically expect the logout to feel instant but in our case we still need to make sure diff --git a/server/main.go b/server/main.go index 6227e725..a3236b2d 100644 --- a/server/main.go +++ b/server/main.go @@ -42,6 +42,8 @@ func Init(a App) { middlewares = []Middleware{ApiHeaders, SecureHeaders} session.HandleFunc("/auth/{service}", NewMiddlewareChain(SessionOAuthBackend, middlewares, a)).Methods("GET") session.HandleFunc("/auth/", NewMiddlewareChain(SessionAuthMiddleware, middlewares, a)).Methods("GET", "POST") + middlewares = []Middleware{ApiHeaders, BodyParser} + r.HandleFunc("/api/token", NewMiddlewareChain(SessionAuthenticateExternal, middlewares, a)).Methods("POST") // API for Admin Console middlewares = []Middleware{ApiHeaders, SecureAjax} @@ -62,7 +64,7 @@ func Init(a App) { files.HandleFunc("/zip", NewMiddlewareChain(FileDownloader, middlewares, a)).Methods("GET") middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureAjax, SessionStart, LoggedInOnly} files.HandleFunc("/cat", NewMiddlewareChain(FileAccess, middlewares, a)).Methods("OPTIONS") - files.HandleFunc("/cat", NewMiddlewareChain(FileSave, middlewares, a)).Methods("POST") + files.HandleFunc("/cat", NewMiddlewareChain(FileSave, middlewares, a)).Methods("POST", "PUT") files.HandleFunc("/ls", NewMiddlewareChain(FileLs, middlewares, a)).Methods("GET") files.HandleFunc("/mv", NewMiddlewareChain(FileMv, middlewares, a)).Methods("GET") files.HandleFunc("/rm", NewMiddlewareChain(FileRm, middlewares, a)).Methods("GET") diff --git a/server/middleware/http.go b/server/middleware/http.go index b9581e3b..cd7eef37 100644 --- a/server/middleware/http.go +++ b/server/middleware/http.go @@ -80,11 +80,14 @@ func SecureHeaders(fn func(*App, http.ResponseWriter, *http.Request)) func(ctx * func SecureAjax(fn func(*App, http.ResponseWriter, *http.Request)) func(ctx *App, res http.ResponseWriter, req *http.Request) { return func(ctx *App, res http.ResponseWriter, req *http.Request) { - if req.Header.Get("X-Requested-With") != "XmlHttpRequest" { - Log.Warning("Intrusion detection: %s - %s", req.RemoteAddr, req.URL.String()) - SendErrorResult(res, ErrNotAllowed) + if req.Header.Get("Authorization") != "" { + fn(ctx, res, req) + return + } else if req.Header.Get("X-Requested-With") == "XmlHttpRequest" { + fn(ctx, res, req) return } - fn(ctx, res, req) + Log.Warning("Intrusion detection: %s - %s", req.RemoteAddr, req.URL.String()) + SendErrorResult(res, ErrNotAllowed) } } diff --git a/server/middleware/session.go b/server/middleware/session.go index 38b43af6..18505174 100644 --- a/server/middleware/session.go +++ b/server/middleware/session.go @@ -11,6 +11,7 @@ import ( "net/http" "regexp" "strings" + "time" ) func LoggedInOnly(fn func(*App, http.ResponseWriter, *http.Request)) func(ctx *App, res http.ResponseWriter, req *http.Request) { @@ -239,7 +240,7 @@ func _extractSession(req *http.Request, ctx *App) (map[string]string, error) { var err error var session map[string]string = make(map[string]string) - if ctx.Share.Id != "" { + if ctx.Share.Id != "" { // Shared link str, err = DecryptString(SECRET_KEY_DERIVATE_FOR_USER, ctx.Share.Auth) if err != nil { // This typically happen when changing the secret key @@ -262,28 +263,51 @@ func _extractSession(req *http.Request, ctx *App) (map[string]string, error) { session["path"] = strings.TrimSuffix(ctx.Share.Path, path) + "/" } return session, err - } else { - str := "" - index := 0 - for { - cookie, err := req.Cookie(CookieName(index)) - if err != nil { - break - } - index++ - str += cookie.Value - } - if str == "" { - return session, nil - } - str, err = DecryptString(SECRET_KEY_DERIVATE_FOR_USER, str) - if err != nil { - // This typically happen when changing the secret key - return session, nil - } - err = json.Unmarshal([]byte(str), &session) - return session, err } + + authHeader := req.Header.Get("Authorization") + if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") { // API request + bearer := strings.TrimPrefix(req.Header.Get("Authorization"), "Bearer ") + str, err = DecryptString(SECRET_KEY_DERIVATE_FOR_USER, bearer) + if err != nil { + return session, nil + } + if err = json.Unmarshal([]byte(str), &session); err != nil { + return session, err + } + t, err := time.Parse(time.RFC3339, session["timestamp"]) + if err != nil { + return session, err + } + if IsApiKeyValid(session["api_key"]) == false { + Log.Warning("attempt to use a non valid api key %s", session["api_key"]) + return session, ErrNotValid + } else if t.Add(EXPIRATION_API_TOKEN * time.Second).Before(time.Now()) { + return session, NewError("Access Token has expired", 401) + } + return session, nil + } + + str = "" + index := 0 + for { + cookie, err := req.Cookie(CookieName(index)) + if err != nil { + break + } + index++ + str += cookie.Value + } + if str == "" { + return session, nil + } + str, err = DecryptString(SECRET_KEY_DERIVATE_FOR_USER, str) + if err != nil { + // This typically happen when changing the secret key + return session, nil + } + err = json.Unmarshal([]byte(str), &session) + return session, err } func _extractBackend(req *http.Request, ctx *App) (IBackend, error) {