mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-10-28 04:05:21 +08:00
464 lines
13 KiB
Go
464 lines
13 KiB
Go
package ctrl
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
. "github.com/mickael-kerjean/filestash/server/common"
|
|
"github.com/mickael-kerjean/filestash/server/middleware"
|
|
"github.com/mickael-kerjean/filestash/server/model"
|
|
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
type Session struct {
|
|
Home *string `json:"home,omitempty"`
|
|
IsAuth bool `json:"is_authenticated"`
|
|
Backend string `json:"backendID"`
|
|
Authorization string `json:"authorization,omitempty"`
|
|
}
|
|
|
|
func SessionGet(ctx *App, res http.ResponseWriter, req *http.Request) {
|
|
r := Session{
|
|
IsAuth: false,
|
|
}
|
|
if ctx.Backend == nil {
|
|
SendSuccessResult(res, r)
|
|
return
|
|
}
|
|
home, err := model.GetHome(ctx.Backend, ctx.Session["path"])
|
|
if err != nil {
|
|
SendSuccessResult(res, r)
|
|
return
|
|
} else if ctx.Share.Id != "" {
|
|
home = "/"
|
|
}
|
|
r.IsAuth = true
|
|
r.Home = NewString(home)
|
|
r.Backend = GenerateID(ctx.Session)
|
|
if ctx.Share.Id == "" && Config.Get("features.protection.enable_chromecast").Bool() {
|
|
r.Authorization = ctx.Authorization
|
|
}
|
|
SendSuccessResult(res, r)
|
|
}
|
|
|
|
func SessionAuthenticate(ctx *App, res http.ResponseWriter, req *http.Request) {
|
|
ctx.Body["timestamp"] = time.Now().Format(time.RFC3339)
|
|
session := model.MapStringInterfaceToMapStringString(ctx.Body)
|
|
session["path"] = EnforceDirectory(session["path"])
|
|
|
|
backend, err := model.NewBackend(ctx, session)
|
|
if err != nil {
|
|
Log.Debug("session::auth 'NewBackend' %+v", err)
|
|
SendErrorResult(res, err)
|
|
return
|
|
}
|
|
|
|
if obj, ok := backend.(interface {
|
|
OAuthToken(*map[string]interface{}) error
|
|
}); ok {
|
|
err := obj.OAuthToken(&ctx.Body)
|
|
if err != nil {
|
|
Log.Debug("session::auth 'OAuthToken' %+v", err)
|
|
SendErrorResult(res, NewError("Can't authenticate (OAuth error)", 401))
|
|
return
|
|
}
|
|
session = model.MapStringInterfaceToMapStringString(ctx.Body)
|
|
backend, err = model.NewBackend(ctx, session)
|
|
if err != nil {
|
|
Log.Debug("session::auth 'OAuthToken::NewBackend' %+v", err)
|
|
SendErrorResult(res, NewError("Can't authenticate", 401))
|
|
return
|
|
}
|
|
}
|
|
|
|
home, err := model.GetHome(backend, session["path"])
|
|
if err != nil {
|
|
Log.Debug("session::auth 'GetHome' %+v", err)
|
|
SendErrorResult(res, ErrAuthenticationFailed)
|
|
return
|
|
}
|
|
|
|
s, err := json.Marshal(session)
|
|
if err != nil {
|
|
Log.Debug("session::auth '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 'Encryption' %+v", err)
|
|
SendErrorResult(res, NewError(err.Error(), 500))
|
|
return
|
|
}
|
|
// split session cookie if greater than 3800 bytes
|
|
value_limit := 3800
|
|
index := 0
|
|
end := 0
|
|
for {
|
|
if len(obfuscate) >= (index+1)*value_limit {
|
|
end = (index + 1) * value_limit
|
|
} else {
|
|
end = len(obfuscate)
|
|
}
|
|
c := http.Cookie{
|
|
Name: CookieName(index),
|
|
Value: obfuscate[index*value_limit : end],
|
|
MaxAge: 60 * Config.Get("general.cookie_timeout").Int(),
|
|
Path: COOKIE_PATH,
|
|
}
|
|
http.SetCookie(res, applyCookieRules(c, req))
|
|
if end == len(obfuscate) {
|
|
break
|
|
} else {
|
|
Log.Debug("session::auth obfuscate index: %d length: %d total: %d", index, len(obfuscate[index*value_limit:end]), len(obfuscate))
|
|
index++
|
|
}
|
|
}
|
|
if Config.Get("features.protection.iframe").String() != "" {
|
|
res.Header().Set("bearer", obfuscate)
|
|
}
|
|
if home != "" {
|
|
SendSuccessResult(res, home)
|
|
return
|
|
}
|
|
SendSuccessResult(res, nil)
|
|
}
|
|
|
|
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
|
|
// the connection is closed as lot of backend requires to hold an active session which we cache.
|
|
// Whenever somebody logout after say 30 minutes idle, the logout would first create a connection
|
|
// then close which can take a few seconds and make for a bad user experience.
|
|
// By pushing that connection close in a goroutine, we make sure the logout is much faster for
|
|
// the user while still retaining that functionality.
|
|
middleware.SessionTry(func(c *App, _res http.ResponseWriter, _req *http.Request) {
|
|
if c.Backend != nil {
|
|
if obj, ok := c.Backend.(interface{ Close() error }); ok {
|
|
obj.Close()
|
|
}
|
|
}
|
|
})(ctx, res, req)
|
|
}()
|
|
index := 0
|
|
for {
|
|
_, err := req.Cookie(CookieName(index))
|
|
if err != nil {
|
|
break
|
|
}
|
|
http.SetCookie(res, applyCookieRules(http.Cookie{
|
|
Name: CookieName(index),
|
|
Value: "",
|
|
MaxAge: -1,
|
|
Path: COOKIE_PATH,
|
|
}, req))
|
|
index++
|
|
}
|
|
http.SetCookie(res, &http.Cookie{
|
|
Name: COOKIE_NAME_ADMIN,
|
|
Value: "",
|
|
MaxAge: -1,
|
|
Path: COOKIE_PATH_ADMIN,
|
|
})
|
|
http.SetCookie(res, &http.Cookie{
|
|
Name: COOKIE_NAME_PROOF,
|
|
Value: "",
|
|
MaxAge: -1,
|
|
Path: COOKIE_PATH,
|
|
})
|
|
SendSuccessResult(res, nil)
|
|
}
|
|
|
|
func SessionOAuthBackend(ctx *App, res http.ResponseWriter, req *http.Request) {
|
|
vars := mux.Vars(req)
|
|
a := map[string]string{
|
|
"type": vars["service"],
|
|
}
|
|
b, err := model.NewBackend(ctx, a)
|
|
if err != nil {
|
|
Log.Debug("session::oauth 'NewBackend' %+v", err)
|
|
SendErrorResult(res, err)
|
|
return
|
|
}
|
|
obj, ok := b.(interface{ OAuthURL() string })
|
|
if ok == false {
|
|
Log.Debug("session::oauth 'Backend does not support oauth - \"%s\"'", a["type"])
|
|
SendErrorResult(res, ErrNotSupported)
|
|
return
|
|
}
|
|
redirectUrl, err := url.Parse(obj.OAuthURL())
|
|
if err != nil {
|
|
Log.Debug("session::oauth 'Parse URL - \"%s\"'", a["type"])
|
|
SendErrorResult(res, ErrNotValid)
|
|
return
|
|
}
|
|
stateValue := vars["service"]
|
|
if req.URL.Query().Get("next") != "" {
|
|
stateValue += "::" + req.URL.Query().Get("next")
|
|
}
|
|
q := redirectUrl.Query()
|
|
q.Set("state", stateValue)
|
|
redirectUrl.RawQuery = q.Encode()
|
|
if strings.Contains(req.Header.Get("Accept"), "text/html") {
|
|
http.Redirect(res, req, redirectUrl.String(), http.StatusSeeOther)
|
|
return
|
|
}
|
|
SendSuccessResult(res, redirectUrl.String())
|
|
}
|
|
|
|
func SessionAuthMiddleware(ctx *App, res http.ResponseWriter, req *http.Request) {
|
|
SSOCookieName := "ssoref"
|
|
|
|
// Step0: Initialisation
|
|
_get := req.URL.Query()
|
|
plugin := func() IAuthentication {
|
|
selectedPluginId := Config.Get("middleware.identity_provider.type").String()
|
|
if selectedPluginId == "" {
|
|
return nil
|
|
}
|
|
for key, plugin := range Hooks.Get.AuthenticationMiddleware() {
|
|
if key == selectedPluginId {
|
|
return plugin
|
|
}
|
|
}
|
|
return nil
|
|
}()
|
|
if plugin == nil {
|
|
http.Redirect(
|
|
res, req,
|
|
"/?error=Not%20Found&trace=middleware not found",
|
|
http.StatusTemporaryRedirect,
|
|
)
|
|
return
|
|
}
|
|
formData := map[string]string{}
|
|
switch req.Method {
|
|
case "GET":
|
|
for key, element := range _get {
|
|
if len(element) == 0 {
|
|
continue
|
|
}
|
|
formData[key] = element[0]
|
|
}
|
|
case "POST":
|
|
if err := req.ParseForm(); err != nil {
|
|
http.Redirect(
|
|
res, req,
|
|
"/?error=Not%20Valid&trace=parsing body - "+err.Error(),
|
|
http.StatusTemporaryRedirect,
|
|
)
|
|
return
|
|
}
|
|
for key, values := range req.Form {
|
|
if len(values) == 0 {
|
|
continue
|
|
}
|
|
formData[key] = values[0]
|
|
}
|
|
}
|
|
idpParams := map[string]string{}
|
|
if err := json.Unmarshal(
|
|
[]byte(Config.Get("middleware.identity_provider.params").String()),
|
|
&idpParams,
|
|
); err != nil {
|
|
http.Redirect(
|
|
res, req,
|
|
"/?error=Not%20Valid&trace=unpacking idp - "+err.Error(),
|
|
http.StatusTemporaryRedirect,
|
|
)
|
|
return
|
|
}
|
|
|
|
// Step1: Entrypoint of the authentication process is handled by the plugin
|
|
if req.Method == "GET" && _get.Get("action") == "redirect" {
|
|
if label := _get.Get("label"); label != "" {
|
|
http.SetCookie(res, applyCookieRules(http.Cookie{
|
|
Name: SSOCookieName,
|
|
Value: label + "::" + _get.Get("state"),
|
|
MaxAge: 60 * 10,
|
|
Path: COOKIE_PATH,
|
|
}, req))
|
|
}
|
|
if err := plugin.EntryPoint(idpParams, req, res); err != nil {
|
|
Log.Error("entrypoint - %s", err.Error())
|
|
res.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
res.WriteHeader(http.StatusOK)
|
|
res.Write([]byte(Page(err.Error())))
|
|
}
|
|
return
|
|
}
|
|
|
|
// Step2: End of the authentication process. Could come from:
|
|
// - target of a html form. eg: ldap, mysql, ...
|
|
// - identity provider redirection uri. eg: oauth2, openid, ...
|
|
templateBind, err := plugin.Callback(formData, idpParams, res)
|
|
if err == ErrAuthenticationFailed {
|
|
http.Redirect(
|
|
res, req,
|
|
req.URL.Path+"?action=redirect",
|
|
http.StatusSeeOther,
|
|
)
|
|
return
|
|
} else if err != nil {
|
|
Log.Error("session::authMiddleware 'callback error - %s'", err.Error())
|
|
http.Redirect(
|
|
res, req,
|
|
"/?error="+ErrNotAllowed.Error()+"&trace=redirect request failed - "+err.Error(),
|
|
http.StatusSeeOther,
|
|
)
|
|
return
|
|
}
|
|
for _, value := range os.Environ() {
|
|
pair := strings.SplitN(value, "=", 2)
|
|
if len(pair) == 2 {
|
|
templateBind[fmt.Sprintf("ENV_%s", pair[0])] = pair[1]
|
|
}
|
|
}
|
|
templateBind["machine_id"] = GenerateMachineID()
|
|
|
|
cookieLabel := ""
|
|
if refCookie, err := req.Cookie(SSOCookieName); err == nil {
|
|
s := strings.SplitN(refCookie.Value, "::", 2)
|
|
switch len(s) {
|
|
case 1:
|
|
cookieLabel = s[0]
|
|
case 2:
|
|
cookieLabel = s[0]
|
|
if decodedState, err := base64.StdEncoding.DecodeString(s[1]); err == nil {
|
|
cookieState := map[string]string{}
|
|
json.Unmarshal(decodedState, &cookieState)
|
|
for key, value := range cookieState {
|
|
if templateBind[key] != "" {
|
|
continue
|
|
}
|
|
templateBind[key] = value
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step3: create a backend connection object
|
|
session, err := func(tb map[string]string) (map[string]string, error) {
|
|
globalMapping := map[string]map[string]interface{}{}
|
|
if err = json.Unmarshal(
|
|
[]byte(Config.Get("middleware.attribute_mapping.params").String()),
|
|
&globalMapping,
|
|
); err != nil {
|
|
Log.Warning("session::authMiddlware 'attribute mapping error' %s", err.Error())
|
|
return map[string]string{}, err
|
|
}
|
|
mappingToUse := map[string]string{}
|
|
for k, v := range globalMapping[cookieLabel] {
|
|
str := NewStringFromInterface(v)
|
|
if str == "" {
|
|
continue
|
|
}
|
|
tmpl, err := template.
|
|
New("ctrl::session::auth_middleware").
|
|
Funcs(map[string]interface{}{
|
|
"contains": func(str string, match string) bool {
|
|
splits := strings.Split(str, ",")
|
|
for _, split := range splits {
|
|
if split == match {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
},
|
|
"encryptGCM": func(str string, key string) (string, error) {
|
|
data, err := EncryptAESGCM([]byte(key), []byte(str))
|
|
return base64.StdEncoding.EncodeToString(data), err
|
|
},
|
|
}).
|
|
Parse(str)
|
|
mappingToUse[k] = str
|
|
if err != nil {
|
|
Log.Debug("session::authMiddleware 'template creation failed %s'", err.Error())
|
|
continue
|
|
}
|
|
var b bytes.Buffer
|
|
if err = tmpl.Execute(&b, tb); err != nil {
|
|
Log.Debug("session::authMiddleware 'template execution failed %s'", err.Error())
|
|
continue
|
|
}
|
|
mappingToUse[k] = b.String()
|
|
}
|
|
mappingToUse["timestamp"] = time.Now().Format(time.RFC3339)
|
|
return mappingToUse, nil
|
|
}(templateBind)
|
|
if err != nil {
|
|
Log.Debug("session::authMiddleware 'auth mapping failed %s'", err.Error())
|
|
http.Redirect(
|
|
res, req,
|
|
WithBase("/?error=Not%20Valid&trace=mapping_error - "+err.Error()),
|
|
http.StatusTemporaryRedirect,
|
|
)
|
|
return
|
|
}
|
|
|
|
if _, err := model.NewBackend(ctx, session); err != nil {
|
|
Log.Debug("session::authMiddleware 'backend connection failed %+v - %s'", session, err.Error())
|
|
url := "/?error=" + ErrNotValid.Error() + "&trace=backend error - " + err.Error()
|
|
if IsATranslatedError(err) {
|
|
url = "/?error=" + err.Error() + "&trace=backend error - " + err.Error()
|
|
}
|
|
http.Redirect(res, req, WithBase(url), http.StatusTemporaryRedirect)
|
|
return
|
|
}
|
|
|
|
// Step4: persist connection with a cookie
|
|
s, err := json.Marshal(session)
|
|
if err != nil {
|
|
Log.Debug("session::authMiddleware 'session marshal error %+v'", session)
|
|
SendErrorResult(res, ErrNotValid)
|
|
return
|
|
}
|
|
obfuscate, err := EncryptString(SECRET_KEY_DERIVATE_FOR_USER, string(s))
|
|
if err != nil {
|
|
Log.Debug("session::authMiddleware 'encryption error - %s", err.Error())
|
|
SendErrorResult(res, ErrNotValid)
|
|
return
|
|
}
|
|
http.SetCookie(res, applyCookieRules(http.Cookie{
|
|
Name: COOKIE_NAME_AUTH,
|
|
Value: obfuscate,
|
|
MaxAge: 60 * Config.Get("general.cookie_timeout").Int(),
|
|
Path: COOKIE_PATH,
|
|
}, req))
|
|
http.SetCookie(res, applyCookieRules(http.Cookie{
|
|
Name: SSOCookieName,
|
|
Value: "",
|
|
MaxAge: -1,
|
|
Path: COOKIE_PATH,
|
|
}, req))
|
|
redirectURI := templateBind["next"]
|
|
if redirectURI == "" {
|
|
redirectURI = WithBase("/")
|
|
}
|
|
http.Redirect(res, req, redirectURI, http.StatusTemporaryRedirect)
|
|
}
|
|
|
|
func applyCookieRules(cookie http.Cookie, req *http.Request) *http.Cookie {
|
|
cookie.HttpOnly = true
|
|
cookie.SameSite = http.SameSiteStrictMode
|
|
if Config.Get("features.protection.iframe").String() != "" {
|
|
if f := req.Header.Get("Referer"); strings.HasPrefix(f, "https://") {
|
|
cookie.Secure = true
|
|
cookie.SameSite = http.SameSiteNoneMode
|
|
cookie.Partitioned = true
|
|
} else {
|
|
Log.Warning("you are trying to access Filestash from a non secure origin ('%s') and with iframe enabled. Either use SSL or disable iframe from the admin console.", f)
|
|
}
|
|
}
|
|
return &cookie
|
|
}
|