From 9d596704e73a36ca90e16bbf40d031d9387815d4 Mon Sep 17 00:00:00 2001 From: Mickael Kerjean Date: Mon, 19 Sep 2022 23:13:38 +1000 Subject: [PATCH] feature (api): public api --- client/model/files.js | 2 +- server/common/constants.go | 33 +++++++++---------- server/common/response.go | 2 +- server/ctrl/session.go | 62 ------------------------------------ server/ctrl/static.go | 9 ------ server/main.go | 37 ++++++++++----------- server/middleware/http.go | 48 ++++++++++++++++++++++------ server/middleware/session.go | 46 +++++++------------------- 8 files changed, 82 insertions(+), 157 deletions(-) diff --git a/client/model/files.js b/client/model/files.js index 720de696..043bc943 100644 --- a/client/model/files.js +++ b/client/model/files.js @@ -55,7 +55,7 @@ class FileSystem { access_count: 0, metadata: null, }, _files); - store.metadata = response.metadata; + store.metadata = response.permissions; store.results = response.results; if (_files && _files.results) { diff --git a/server/common/constants.go b/server/common/constants.go index a5f41c50..9dc544f1 100644 --- a/server/common/constants.go +++ b/server/common/constants.go @@ -6,22 +6,21 @@ 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" - EXPIRATION_API_TOKEN = 3600 * 24 * 365 // 1 year + 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" ) func init() { @@ -40,7 +39,6 @@ var ( SECRET_KEY_DERIVATE_FOR_PROOF string SECRET_KEY_DERIVATE_FOR_ADMIN string SECRET_KEY_DERIVATE_FOR_USER string - SECRET_KEY_DERIVATE_FOR_API string SECRET_KEY_DERIVATE_FOR_HASH string ) @@ -53,6 +51,5 @@ func InitSecretDerivate(secret string) { SECRET_KEY_DERIVATE_FOR_PROOF = Hash("PROOF_"+SECRET_KEY, len(SECRET_KEY)) SECRET_KEY_DERIVATE_FOR_ADMIN = Hash("ADMIN_"+SECRET_KEY, len(SECRET_KEY)) SECRET_KEY_DERIVATE_FOR_USER = Hash("USER_"+SECRET_KEY, len(SECRET_KEY)) - SECRET_KEY_DERIVATE_FOR_API = Hash("API_"+SECRET_KEY, len(SECRET_KEY)) SECRET_KEY_DERIVATE_FOR_HASH = Hash("HASH_"+SECRET_KEY, len(SECRET_KEY)) } diff --git a/server/common/response.go b/server/common/response.go index 865fb435..c730bd60 100644 --- a/server/common/response.go +++ b/server/common/response.go @@ -23,7 +23,7 @@ type APISuccessResults struct { type APISuccessResultsWithMetadata struct { Status string `json:"status"` Results interface{} `json:"results"` - Metadata interface{} `json:"metadata,omitempty"` + Metadata interface{} `json:"permissions,omitempty"` } type APIErrorMessage struct { diff --git a/server/ctrl/session.go b/server/ctrl/session.go index e71e5047..8ba6ce02 100644 --- a/server/ctrl/session.go +++ b/server/ctrl/session.go @@ -128,68 +128,6 @@ func SessionAuthenticate(ctx *App, res http.ResponseWriter, req *http.Request) { SendSuccessResult(res, nil) } -func SessionAuthenticateExternal(ctx *App, res http.ResponseWriter, req *http.Request) { - h := res.Header() - h.Set("X-Request-ID", middleware.GenerateRequestID("API")) - - api_key := string(req.URL.Query().Get("key")) - if api_key == "" { - middleware.EnableCors(req, res, "*") - 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 - } - host, err := VerifyApiKey(api_key) - if err != nil { - middleware.EnableCors(req, res, "*") - SendErrorResult(res, NewError(fmt.Sprintf( - "Invalid API Key provided: '%s'", - api_key, - ), 401)) - return - } - if err = middleware.EnableCors(req, res, host); err != nil { - middleware.EnableCors(req, res, "*") - SendErrorResult(res, err) - 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_API, 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"` - ApiDoc string `json:"doc"` - }{ - obfuscate, "bearer", - EXPIRATION_API_TOKEN, - fmt.Sprintf("Filestash %s.%s", APP_VERSION, BUILD_DATE), - "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/ctrl/static.go b/server/ctrl/static.go index edf67fe2..0b065cc5 100644 --- a/server/ctrl/static.go +++ b/server/ctrl/static.go @@ -4,7 +4,6 @@ import ( _ "embed" "fmt" . "github.com/mickael-kerjean/filestash/server/common" - "github.com/mickael-kerjean/filestash/server/middleware" "io" "net/http" URL "net/url" @@ -78,14 +77,6 @@ func NotFoundHandler(ctx *App, res http.ResponseWriter, req *http.Request) { SendErrorResult(res, ErrNotFound) } -func PreflightCorsOK(ctx *App, res http.ResponseWriter, req *http.Request) { - if err := middleware.EnableCors(req, res, "*"); err != nil { - SendErrorResult(res, err) - return - } - SendSuccessResult(res, nil) -} - var listOfPlugins map[string][]string = map[string][]string{ "oss": []string{}, "enterprise": []string{}, diff --git a/server/main.go b/server/main.go index 67c6a6ad..8736c658 100644 --- a/server/main.go +++ b/server/main.go @@ -33,27 +33,23 @@ func Init(a App) { // API for Session session := r.PathPrefix("/api/session").Subrouter() - middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureAjax, SessionStart} + middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureOrigin, SessionStart} session.HandleFunc("", NewMiddlewareChain(SessionGet, middlewares, a)).Methods("GET") - middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureAjax, RateLimiter, BodyParser} + middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureOrigin, RateLimiter, BodyParser} session.HandleFunc("", NewMiddlewareChain(SessionAuthenticate, middlewares, a)).Methods("POST") - middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureAjax} + middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureOrigin} session.HandleFunc("", NewMiddlewareChain(SessionLogout, middlewares, a)).Methods("DELETE") middlewares = []Middleware{ApiHeaders, SecureHeaders} session.HandleFunc("/auth/{service}", NewMiddlewareChain(SessionOAuthBackend, middlewares, a)).Methods("GET") session.HandleFunc("/auth/", NewMiddlewareChain(SessionAuthMiddleware, middlewares, a)).Methods("GET", "POST") - token := r.PathPrefix("/api/token").Subrouter() - middlewares = []Middleware{ApiHeaders, RateLimiter, BodyParser} - token.HandleFunc("", NewMiddlewareChain(SessionAuthenticateExternal, middlewares, a)).Methods("POST") - token.HandleFunc("", NewMiddlewareChain(PreflightCorsOK, []Middleware{}, a)).Methods("OPTIONS") // API for Admin Console admin := r.PathPrefix("/admin/api").Subrouter() - middlewares = []Middleware{ApiHeaders, SecureAjax} + middlewares = []Middleware{ApiHeaders, SecureOrigin} admin.HandleFunc("/session", NewMiddlewareChain(AdminSessionGet, middlewares, a)).Methods("GET") - middlewares = []Middleware{ApiHeaders, SecureAjax, RateLimiter} + middlewares = []Middleware{ApiHeaders, SecureOrigin, RateLimiter} admin.HandleFunc("/session", NewMiddlewareChain(AdminSessionAuthenticate, middlewares, a)).Methods("POST") - middlewares = []Middleware{ApiHeaders, AdminOnly, SecureAjax} + middlewares = []Middleware{ApiHeaders, AdminOnly, SecureOrigin} admin.HandleFunc("/config", NewMiddlewareChain(PrivateConfigHandler, middlewares, a)).Methods("GET") admin.HandleFunc("/config", NewMiddlewareChain(PrivateConfigUpdateHandler, middlewares, a)).Methods("POST") admin.HandleFunc("/audit", NewMiddlewareChain(FetchAuditHandler, middlewares, a)).Methods("GET") @@ -62,10 +58,10 @@ func Init(a App) { // API for File management files := r.PathPrefix("/api/files").Subrouter() - middlewares = []Middleware{ApiHeaders, SecureHeaders, SessionStart, LoggedInOnly} + middlewares = []Middleware{ApiHeaders, SecureHeaders, WithPublicAPI, SessionStart, LoggedInOnly} files.HandleFunc("/cat", NewMiddlewareChain(FileCat, middlewares, a)).Methods("GET", "HEAD") files.HandleFunc("/zip", NewMiddlewareChain(FileDownloader, middlewares, a)).Methods("GET") - middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureAjax, SessionStart, LoggedInOnly} + middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureOrigin, WithPublicAPI, SessionStart, LoggedInOnly} files.HandleFunc("/cat", NewMiddlewareChain(FileAccess, middlewares, a)).Methods("OPTIONS") files.HandleFunc("/cat", NewMiddlewareChain(FileSave, middlewares, a)).Methods("POST") files.HandleFunc("/ls", NewMiddlewareChain(FileLs, middlewares, a)).Methods("GET") @@ -73,19 +69,18 @@ func Init(a App) { files.HandleFunc("/rm", NewMiddlewareChain(FileRm, middlewares, a)).Methods("POST") files.HandleFunc("/mkdir", NewMiddlewareChain(FileMkdir, middlewares, a)).Methods("POST") files.HandleFunc("/touch", NewMiddlewareChain(FileTouch, middlewares, a)).Methods("POST") - middlewares = []Middleware{ApiHeaders, SessionStart, LoggedInOnly} + middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureOrigin, WithPublicAPI, SessionStart, LoggedInOnly} files.HandleFunc("/search", NewMiddlewareChain(FileSearch, middlewares, a)).Methods("GET") - r.PathPrefix("/api/files").Handler(NewMiddlewareChain(PreflightCorsOK, []Middleware{}, a)).Methods("OPTIONS") // API for Shared link share := r.PathPrefix("/api/share").Subrouter() - middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureAjax, SessionStart, LoggedInOnly} + middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureOrigin, SessionStart, LoggedInOnly} share.HandleFunc("", NewMiddlewareChain(ShareList, middlewares, a)).Methods("GET") - middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureAjax, BodyParser} + middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureOrigin, BodyParser} share.HandleFunc("/{share}/proof", NewMiddlewareChain(ShareVerifyProof, middlewares, a)).Methods("POST") - middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureAjax, CanManageShare} + middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureOrigin, CanManageShare} share.HandleFunc("/{share}", NewMiddlewareChain(ShareDelete, middlewares, a)).Methods("DELETE") - middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureAjax, BodyParser, CanManageShare} + middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureOrigin, BodyParser, CanManageShare} share.HandleFunc("/{share}", NewMiddlewareChain(ShareUpsert, middlewares, a)).Methods("POST") // Webdav server / Shared Link @@ -97,11 +92,11 @@ func Init(a App) { r.PathPrefix("/api/export/{share}/{mtype0}/{mtype1}").Handler(NewMiddlewareChain(FileExport, middlewares, a)) // Application Resources - middlewares = []Middleware{ApiHeaders} + middlewares = []Middleware{ApiHeaders, SecureHeaders} r.HandleFunc("/api/config", NewMiddlewareChain(PublicConfigHandler, middlewares, a)).Methods("GET") r.HandleFunc("/api/backend", NewMiddlewareChain(AdminBackend, middlewares, a)).Methods("GET") r.HandleFunc("/api/middlewares/authentication", NewMiddlewareChain(AdminAuthenticationMiddleware, middlewares, a)).Methods("GET") - middlewares = []Middleware{StaticHeaders} + middlewares = []Middleware{StaticHeaders, SecureHeaders} r.PathPrefix("/assets").Handler(http.HandlerFunc(NewMiddlewareChain(StaticHandler(FILE_ASSETS), middlewares, a))).Methods("GET") r.HandleFunc("/favicon.ico", NewMiddlewareChain(StaticHandler(FILE_ASSETS+"/assets/logo/"), middlewares, a)).Methods("GET") r.HandleFunc("/sw_cache.js", NewMiddlewareChain(StaticHandler(FILE_ASSETS+"/assets/worker/"), middlewares, a)).Methods("GET") @@ -109,7 +104,7 @@ func Init(a App) { // Other endpoints middlewares = []Middleware{ApiHeaders} r.HandleFunc("/report", NewMiddlewareChain(ReportHandler, middlewares, a)).Methods("POST") - middlewares = []Middleware{IndexHeaders} + middlewares = []Middleware{IndexHeaders, SecureHeaders} r.HandleFunc("/about", NewMiddlewareChain(AboutHandler, middlewares, a)).Methods("GET") r.HandleFunc("/robots.txt", NewMiddlewareChain(RobotsHandler, []Middleware{}, a)) r.HandleFunc("/manifest.json", NewMiddlewareChain(ManifestHandler, []Middleware{}, a)).Methods("GET") diff --git a/server/middleware/http.go b/server/middleware/http.go index 53d2b846..246e05f7 100644 --- a/server/middleware/http.go +++ b/server/middleware/http.go @@ -68,13 +68,6 @@ func IndexHeaders(fn func(*App, http.ResponseWriter, *http.Request)) func(ctx *A func SecureHeaders(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 host := Config.Get("general.host").String(); host != "" { - if req.Host != host && req.Host != fmt.Sprintf("%s:443", host) { - Log.Error("Request coming from \"%s\" was blocked, only traffic from \"%s\" is allowed. You can change this from the admin console under configure -> host", req.Host, host) - SendErrorResult(res, ErrNotAllowed) - return - } - } header := res.Header() if Config.Get("general.force_ssl").Bool() { header.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload") @@ -85,20 +78,55 @@ 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) { +func SecureOrigin(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("Authorization") != "" { + if host := Config.Get("general.host").String(); host != "" { + if req.Host != host && req.Host != fmt.Sprintf("%s:443", host) { + Log.Error("Request coming from \"%s\" was blocked, only traffic from \"%s\" is allowed. You can change this from the admin console under configure -> host", req.Host, host) + SendErrorResult(res, ErrNotAllowed) + return + } + } + if req.Header.Get("X-Requested-With") == "XmlHttpRequest" { // Browser XHR Access fn(ctx, res, req) return - } else if req.Header.Get("X-Requested-With") == "XmlHttpRequest" { + } else if apiKey := req.URL.Query().Get("key"); apiKey != "" { // API Access fn(ctx, res, req) return } + Log.Warning("Intrusion detection: %s - %s", req.RemoteAddr, req.URL.String()) SendErrorResult(res, ErrNotAllowed) } } +func WithPublicAPI(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) { + apiKey := req.URL.Query().Get("key") + if apiKey == "" { + fn(ctx, res, req) + return + } + res.Header().Set("X-Request-ID", GenerateRequestID("API")) + host, err := VerifyApiKey(apiKey) + if err != nil { + Log.Debug("middleware::http api verification error '%s'", err.Error()) + EnableCors(req, res, "*") + SendErrorResult(res, NewError(fmt.Sprintf( + "Invalid API Key provided: '%s'", + apiKey, + ), 401)) + return + } + if err = EnableCors(req, res, host); err != nil { + EnableCors(req, res, "*") + SendErrorResult(res, err) + return + } + fn(ctx, res, req) + } +} + var limiter = rate.NewLimiter(10, 1000) func RateLimiter(fn func(*App, http.ResponseWriter, *http.Request)) func(ctx *App, res http.ResponseWriter, req *http.Request) { diff --git a/server/middleware/session.go b/server/middleware/session.go index 282a6455..545cab9f 100644 --- a/server/middleware/session.go +++ b/server/middleware/session.go @@ -57,7 +57,7 @@ func SessionStart(fn func(*App, http.ResponseWriter, *http.Request)) func(ctx *A SendErrorResult(res, err) return } - if ctx.Session, err = _extractSession(req, res, ctx); err != nil { + if ctx.Session, err = _extractSession(req, ctx); err != nil { SendErrorResult(res, err) return } @@ -76,7 +76,7 @@ func SessionStart(fn func(*App, http.ResponseWriter, *http.Request)) func(ctx *A func SessionTry(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) { ctx.Share, _ = _extractShare(req) - ctx.Session, _ = _extractSession(req, res, ctx) + ctx.Session, _ = _extractSession(req, ctx) ctx.Backend, _ = _extractBackend(req, ctx) fn(ctx, res, req) } @@ -128,7 +128,7 @@ func CanManageShare(fn func(*App, http.ResponseWriter, *http.Request)) func(ctx // the user that's currently logged in can manage the link. 2 scenarios here: // 1) scenario 1: the user is the very same one that generated the shared link in the first place ctx.Share = Share{} - if ctx.Session, err = _extractSession(req, res, ctx); err != nil { + if ctx.Session, err = _extractSession(req, ctx); err != nil { Log.Debug("middleware::session::share 'cannot extract session - %s'", err.Error()) SendErrorResult(res, err) return @@ -144,7 +144,7 @@ func CanManageShare(fn func(*App, http.ResponseWriter, *http.Request)) func(ctx SendErrorResult(res, err) return } - if ctx.Session, err = _extractSession(req, res, ctx); err != nil { + if ctx.Session, err = _extractSession(req, ctx); err != nil { Log.Debug("middleware::session::share 'cannot extract session 2 - %s'", err.Error()) SendErrorResult(res, err) return @@ -235,7 +235,7 @@ func _extractShare(req *http.Request) (Share, error) { return s, nil } -func _extractSession(req *http.Request, res http.ResponseWriter, ctx *App) (map[string]string, error) { +func _extractSession(req *http.Request, ctx *App) (map[string]string, error) { var str string var err error var session map[string]string = make(map[string]string) @@ -265,36 +265,6 @@ func _extractSession(req *http.Request, res http.ResponseWriter, ctx *App) (map[ 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_API, 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 - } - host, err := VerifyApiKey(session["api_key"]) - if err != nil { - Log.Warning("attempt to use a non valid api key %s", session["api_key"]) - if err == ErrNotValid { - return session, NewError("Your API key is not valid", 401) - } else { - return session, err - } - } else if t.Add(EXPIRATION_API_TOKEN * time.Second).Before(time.Now()) { - return session, NewError("Access Token has expired", 401) - } else if err = EnableCors(req, res, host); err != nil { - return session, err - } - return session, nil - } - str = "" index := 0 for { @@ -305,6 +275,12 @@ func _extractSession(req *http.Request, res http.ResponseWriter, ctx *App) (map[ index++ str += cookie.Value } + if str == "" { + authHeader := req.Header.Get("Authorization") + if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") { + str = strings.TrimPrefix(req.Header.Get("Authorization"), "Bearer ") + } + } if str == "" { return session, nil }