feature (api): prepare api for public use

This commit is contained in:
Mickael Kerjean
2022-09-05 11:59:28 +10:00
parent c7e40e42db
commit 549d6805f0
9 changed files with 79 additions and 12 deletions
+3 -1
View File
@@ -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))
}
+40 -1
View File
@@ -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
}
+6 -5
View File
@@ -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
View File
@@ -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")
+6
View File
@@ -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 -1
View File
@@ -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) {
+2
View File
@@ -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)
+12 -2
View File
@@ -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
}
+3 -1
View File
@@ -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"])