mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2026-10-19 16:14:39 +00:00
feature (api): prepare api for public use
This commit is contained in:
@@ -21,7 +21,7 @@ const (
|
||||
FILE_INDEX = "./data/public/index.html"
|
||||
FILE_ASSETS = "./data/public/"
|
||||
URL_SETUP = "/admin/setup"
|
||||
EXPIRATION_API_TOKEN = 3600
|
||||
EXPIRATION_API_TOKEN = 3600 * 24 * 365 // 1 year
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -40,6 +40,7 @@ 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
|
||||
)
|
||||
|
||||
@@ -52,5 +53,6 @@ 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))
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const IndentSize = " "
|
||||
|
||||
type APISuccessResult struct {
|
||||
Status string `json:"status"`
|
||||
Result interface{} `json:"result,omitempty"`
|
||||
@@ -32,11 +34,26 @@ type APIErrorMessage struct {
|
||||
func SendSuccessResult(res http.ResponseWriter, data interface{}) {
|
||||
encoder := json.NewEncoder(res)
|
||||
encoder.SetEscapeHTML(false)
|
||||
if shouldIndentResponse(res) {
|
||||
encoder.SetIndent("", IndentSize)
|
||||
}
|
||||
encoder.Encode(APISuccessResult{"ok", data})
|
||||
}
|
||||
|
||||
func SendSuccessResultWithEtagAndGzip(res http.ResponseWriter, req *http.Request, data interface{}) {
|
||||
dataToSend, _ := json.Marshal(APISuccessResult{"ok", data})
|
||||
var dataToSend []byte
|
||||
var err error
|
||||
if shouldIndentResponse(res) {
|
||||
if dataToSend, err = json.MarshalIndent(APISuccessResult{"ok", data}, "", IndentSize); err != nil {
|
||||
Log.Warning("common::response Marshal %s", err.Error())
|
||||
dataToSend = []byte("{}")
|
||||
}
|
||||
} else {
|
||||
if dataToSend, err = json.Marshal(APISuccessResult{"ok", data}); err != nil {
|
||||
Log.Warning("common::response Marshal %s", err.Error())
|
||||
dataToSend = []byte("{}")
|
||||
}
|
||||
}
|
||||
mode := "normal"
|
||||
if strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") == true {
|
||||
mode = "gzip"
|
||||
@@ -63,18 +80,27 @@ func SendSuccessResultWithEtagAndGzip(res http.ResponseWriter, req *http.Request
|
||||
func SendSuccessResults(res http.ResponseWriter, data interface{}) {
|
||||
encoder := json.NewEncoder(res)
|
||||
encoder.SetEscapeHTML(false)
|
||||
if shouldIndentResponse(res) {
|
||||
encoder.SetIndent("", IndentSize)
|
||||
}
|
||||
encoder.Encode(APISuccessResults{"ok", data})
|
||||
}
|
||||
|
||||
func SendSuccessResultsWithMetadata(res http.ResponseWriter, data interface{}, p interface{}) {
|
||||
encoder := json.NewEncoder(res)
|
||||
encoder.SetEscapeHTML(false)
|
||||
if shouldIndentResponse(res) {
|
||||
encoder.SetIndent("", IndentSize)
|
||||
}
|
||||
encoder.Encode(APISuccessResultsWithMetadata{"ok", data, p})
|
||||
}
|
||||
|
||||
func SendErrorResult(res http.ResponseWriter, err error) {
|
||||
encoder := json.NewEncoder(res)
|
||||
encoder.SetEscapeHTML(false)
|
||||
if shouldIndentResponse(res) {
|
||||
encoder.SetIndent("", IndentSize)
|
||||
}
|
||||
obj, ok := err.(interface{ Status() int })
|
||||
if ok == true {
|
||||
res.WriteHeader(obj.Status())
|
||||
@@ -93,6 +119,9 @@ func SendErrorResult(res http.ResponseWriter, err error) {
|
||||
func SendRaw(res http.ResponseWriter, data interface{}) {
|
||||
encoder := json.NewEncoder(res)
|
||||
encoder.SetEscapeHTML(false)
|
||||
if shouldIndentResponse(res) {
|
||||
encoder.SetIndent("", IndentSize)
|
||||
}
|
||||
encoder.Encode(data)
|
||||
}
|
||||
|
||||
@@ -133,3 +162,13 @@ func RedirectPage(url string) string {
|
||||
</html>
|
||||
`
|
||||
}
|
||||
|
||||
func shouldIndentResponse(res http.ResponseWriter) bool {
|
||||
reqID := res.Header().Get("X-Request-Id")
|
||||
if reqID == "" {
|
||||
return false
|
||||
} else if strings.HasPrefix(reqID, "API") == false {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func SessionGet(ctx *App, res http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
func SessionAuthenticate(ctx *App, res http.ResponseWriter, req *http.Request) {
|
||||
ctx.Body["timestamp"] = time.Now().String()
|
||||
ctx.Body["timestamp"] = time.Now().Format(time.RFC3339)
|
||||
session := model.MapStringInterfaceToMapStringString(ctx.Body)
|
||||
session["path"] = EnforceDirectory(session["path"])
|
||||
|
||||
@@ -129,6 +129,7 @@ func SessionAuthenticate(ctx *App, res http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
func SessionAuthenticateExternal(ctx *App, res http.ResponseWriter, req *http.Request) {
|
||||
res.Header().Set("X-Request-ID", GenerateRequestID("API"))
|
||||
api_key := string(req.URL.Query().Get("key"))
|
||||
if api_key == "" {
|
||||
SendErrorResult(res, NewError(fmt.Sprintf(
|
||||
@@ -159,7 +160,7 @@ func SessionAuthenticateExternal(ctx *App, res http.ResponseWriter, req *http.Re
|
||||
SendErrorResult(res, NewError(err.Error(), 500))
|
||||
return
|
||||
}
|
||||
obfuscate, err := EncryptString(SECRET_KEY_DERIVATE_FOR_USER, string(s))
|
||||
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))
|
||||
@@ -171,12 +172,12 @@ func SessionAuthenticateExternal(ctx *App, res http.ResponseWriter, req *http.Re
|
||||
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/",
|
||||
EXPIRATION_API_TOKEN,
|
||||
fmt.Sprintf("Filestash %s.%s", APP_VERSION, BUILD_DATE),
|
||||
"https://www.filestash.app/docs/api/",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -65,7 +65,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", "PUT")
|
||||
files.HandleFunc("/cat", NewMiddlewareChain(FileSave, middlewares, a)).Methods("POST")
|
||||
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")
|
||||
|
||||
@@ -2,9 +2,11 @@ package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
. "github.com/mickael-kerjean/filestash/server/common"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func BodyParser(fn func(*App, http.ResponseWriter, *http.Request)) func(ctx *App, res http.ResponseWriter, req *http.Request) {
|
||||
@@ -29,3 +31,7 @@ func BodyParser(fn func(*App, http.ResponseWriter, *http.Request)) func(ctx *App
|
||||
fn(ctx, res, req)
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateRequestID(prefix string) string {
|
||||
return fmt.Sprintf("%s::%s", prefix, strings.ToUpper(QuickString(15)))
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"golang.org/x/time/rate"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ApiHeaders(fn func(*App, http.ResponseWriter, *http.Request)) func(ctx *App, res http.ResponseWriter, req *http.Request) {
|
||||
@@ -13,6 +14,10 @@ func ApiHeaders(fn func(*App, http.ResponseWriter, *http.Request)) func(ctx *App
|
||||
header := res.Header()
|
||||
header.Set("Content-Type", "application/json")
|
||||
header.Set("Cache-Control", "no-cache")
|
||||
authHeader := req.Header.Get("Authorization")
|
||||
if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
|
||||
header.Set("X-Request-ID", GenerateRequestID("API"))
|
||||
}
|
||||
fn(ctx, res, req)
|
||||
}
|
||||
}
|
||||
@@ -93,7 +98,7 @@ func SecureAjax(fn func(*App, http.ResponseWriter, *http.Request)) func(ctx *App
|
||||
}
|
||||
}
|
||||
|
||||
var limiter = rate.NewLimiter(5, 500)
|
||||
var limiter = rate.NewLimiter(5, 1000)
|
||||
|
||||
func RateLimiter(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) {
|
||||
|
||||
@@ -69,6 +69,7 @@ type LogEntry struct {
|
||||
Share string `json:"share"`
|
||||
License string `json:"license"`
|
||||
Session string `json:"session"`
|
||||
RequestID string `json:"requestID"`
|
||||
}
|
||||
|
||||
func Logger(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
@@ -104,6 +105,7 @@ func Logger(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
return GenerateID(&ctx)
|
||||
}(),
|
||||
RequestID: res.Header().Get("X-Request-ID"),
|
||||
}
|
||||
if Config.Get("log.telemetry").Bool() {
|
||||
telemetry.Record(point)
|
||||
|
||||
@@ -268,7 +268,7 @@ func _extractSession(req *http.Request, ctx *App) (map[string]string, error) {
|
||||
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)
|
||||
str, err = DecryptString(SECRET_KEY_DERIVATE_FOR_API, bearer)
|
||||
if err != nil {
|
||||
return session, nil
|
||||
}
|
||||
@@ -306,7 +306,17 @@ func _extractSession(req *http.Request, ctx *App) (map[string]string, error) {
|
||||
// This typically happen when changing the secret key
|
||||
return session, nil
|
||||
}
|
||||
err = json.Unmarshal([]byte(str), &session)
|
||||
if err = json.Unmarshal([]byte(str), &session); err != nil {
|
||||
return session, err
|
||||
}
|
||||
t, err := time.Parse(time.RFC3339, session["timestamp"])
|
||||
if err != nil {
|
||||
Log.Warning("middleware::session 'cannot parse time - %s'", err.Error())
|
||||
return session, ErrNotAuthorized
|
||||
} else if t.Add(24 * 365 * time.Hour).Before(time.Now()) {
|
||||
Log.Warning("middleware::session 'cookie too old - %s'", t.Format(time.RFC3339))
|
||||
return session, ErrNotAuthorized
|
||||
}
|
||||
return session, err
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,9 @@ func init() {
|
||||
type TmpStorage struct{}
|
||||
|
||||
func (this TmpStorage) Init(params map[string]string, app *App) (IBackend, error) {
|
||||
if regexp.MustCompile(`^[a-zA-Z0-9]*$`).MatchString(params["userID"]) == false {
|
||||
if len(params["userID"]) == 0 {
|
||||
return nil, ErrAuthenticationFailed
|
||||
} else if regexp.MustCompile(`^[a-zA-Z0-9]*$`).MatchString(params["userID"]) == false {
|
||||
return nil, ErrAuthenticationFailed
|
||||
}
|
||||
p := filepath.Join(FILESTASH_DIRECTORY, params["userID"])
|
||||
|
||||
Reference in New Issue
Block a user