mirror of
https://github.com/mickael-kerjean/filestash
synced 2025-12-08 01:12:49 +01:00
feature (rest): setup for rest api
This commit is contained in:
parent
d1a2c7a3ea
commit
952f45097e
8 changed files with 145 additions and 44 deletions
12
server/common/api.go
Normal file
12
server/common/api.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -85,9 +85,10 @@ func NewKeyValueStore() KeyValueStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (this *KeyValueStore) Get(key string) interface{} {
|
func (this *KeyValueStore) Get(key string) interface{} {
|
||||||
|
var val interface{}
|
||||||
this.RLock()
|
this.RLock()
|
||||||
defer this.RUnlock()
|
val = this.cache[key]
|
||||||
val := this.cache[key]
|
this.RUnlock()
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,21 +6,22 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
APP_VERSION = "v0.5"
|
APP_VERSION = "v0.5"
|
||||||
LOG_PATH = "data/state/log/"
|
LOG_PATH = "data/state/log/"
|
||||||
CONFIG_PATH = "data/state/config/"
|
CONFIG_PATH = "data/state/config/"
|
||||||
DB_PATH = "data/state/db/"
|
DB_PATH = "data/state/db/"
|
||||||
FTS_PATH = "data/state/search/"
|
FTS_PATH = "data/state/search/"
|
||||||
CERT_PATH = "data/state/certs/"
|
CERT_PATH = "data/state/certs/"
|
||||||
TMP_PATH = "data/cache/tmp/"
|
TMP_PATH = "data/cache/tmp/"
|
||||||
COOKIE_NAME_AUTH = "auth"
|
COOKIE_NAME_AUTH = "auth"
|
||||||
COOKIE_NAME_PROOF = "proof"
|
COOKIE_NAME_PROOF = "proof"
|
||||||
COOKIE_NAME_ADMIN = "admin"
|
COOKIE_NAME_ADMIN = "admin"
|
||||||
COOKIE_PATH_ADMIN = "/admin/api/"
|
COOKIE_PATH_ADMIN = "/admin/api/"
|
||||||
COOKIE_PATH = "/api/"
|
COOKIE_PATH = "/api/"
|
||||||
FILE_INDEX = "./data/public/index.html"
|
FILE_INDEX = "./data/public/index.html"
|
||||||
FILE_ASSETS = "./data/public/"
|
FILE_ASSETS = "./data/public/"
|
||||||
URL_SETUP = "/admin/setup"
|
URL_SETUP = "/admin/setup"
|
||||||
|
EXPIRATION_API_TOKEN = 3600
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,12 @@ func SendErrorResult(res http.ResponseWriter, err error) {
|
||||||
encoder.Encode(APIErrorMessage{"error", m})
|
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 {
|
func Page(stuff string) string {
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,58 @@ func SessionAuthenticate(ctx *App, res http.ResponseWriter, req *http.Request) {
|
||||||
SendSuccessResult(res, nil)
|
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) {
|
func SessionLogout(ctx *App, res http.ResponseWriter, req *http.Request) {
|
||||||
go func() {
|
go func() {
|
||||||
// user typically expect the logout to feel instant but in our case we still need to make sure
|
// user typically expect the logout to feel instant but in our case we still need to make sure
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ func Init(a App) {
|
||||||
middlewares = []Middleware{ApiHeaders, SecureHeaders}
|
middlewares = []Middleware{ApiHeaders, SecureHeaders}
|
||||||
session.HandleFunc("/auth/{service}", NewMiddlewareChain(SessionOAuthBackend, middlewares, a)).Methods("GET")
|
session.HandleFunc("/auth/{service}", NewMiddlewareChain(SessionOAuthBackend, middlewares, a)).Methods("GET")
|
||||||
session.HandleFunc("/auth/", NewMiddlewareChain(SessionAuthMiddleware, middlewares, a)).Methods("GET", "POST")
|
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
|
// API for Admin Console
|
||||||
middlewares = []Middleware{ApiHeaders, SecureAjax}
|
middlewares = []Middleware{ApiHeaders, SecureAjax}
|
||||||
|
|
@ -62,7 +64,7 @@ func Init(a App) {
|
||||||
files.HandleFunc("/zip", NewMiddlewareChain(FileDownloader, middlewares, a)).Methods("GET")
|
files.HandleFunc("/zip", NewMiddlewareChain(FileDownloader, middlewares, a)).Methods("GET")
|
||||||
middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureAjax, SessionStart, LoggedInOnly}
|
middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureAjax, SessionStart, LoggedInOnly}
|
||||||
files.HandleFunc("/cat", NewMiddlewareChain(FileAccess, middlewares, a)).Methods("OPTIONS")
|
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("/ls", NewMiddlewareChain(FileLs, middlewares, a)).Methods("GET")
|
||||||
files.HandleFunc("/mv", NewMiddlewareChain(FileMv, middlewares, a)).Methods("GET")
|
files.HandleFunc("/mv", NewMiddlewareChain(FileMv, middlewares, a)).Methods("GET")
|
||||||
files.HandleFunc("/rm", NewMiddlewareChain(FileRm, middlewares, a)).Methods("GET")
|
files.HandleFunc("/rm", NewMiddlewareChain(FileRm, middlewares, a)).Methods("GET")
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
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) {
|
return func(ctx *App, res http.ResponseWriter, req *http.Request) {
|
||||||
if req.Header.Get("X-Requested-With") != "XmlHttpRequest" {
|
if req.Header.Get("Authorization") != "" {
|
||||||
Log.Warning("Intrusion detection: %s - %s", req.RemoteAddr, req.URL.String())
|
fn(ctx, res, req)
|
||||||
SendErrorResult(res, ErrNotAllowed)
|
return
|
||||||
|
} else if req.Header.Get("X-Requested-With") == "XmlHttpRequest" {
|
||||||
|
fn(ctx, res, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fn(ctx, res, req)
|
Log.Warning("Intrusion detection: %s - %s", req.RemoteAddr, req.URL.String())
|
||||||
|
SendErrorResult(res, ErrNotAllowed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LoggedInOnly(fn func(*App, http.ResponseWriter, *http.Request)) func(ctx *App, res http.ResponseWriter, req *http.Request) {
|
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 err error
|
||||||
var session map[string]string = make(map[string]string)
|
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)
|
str, err = DecryptString(SECRET_KEY_DERIVATE_FOR_USER, ctx.Share.Auth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// This typically happen when changing the secret key
|
// 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) + "/"
|
session["path"] = strings.TrimSuffix(ctx.Share.Path, path) + "/"
|
||||||
}
|
}
|
||||||
return session, err
|
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) {
|
func _extractBackend(req *http.Request, ctx *App) (IBackend, error) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue