feature (api): public api

This commit is contained in:
Mickael Kerjean 2022-09-19 23:13:38 +10:00
parent 8ee1372760
commit 9d596704e7
8 changed files with 82 additions and 157 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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