feature (rest): setup for rest api

This commit is contained in:
Mickael Kerjean 2022-09-03 17:48:15 +10:00
parent d1a2c7a3ea
commit 952f45097e
8 changed files with 145 additions and 44 deletions

12
server/common/api.go Normal file
View file

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

View file

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

View file

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

View file

@ -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 `<!DOCTYPE html>
<html>

View file

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

View file

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

View file

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

View file

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