mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2026-10-19 16:14:39 +00:00
optimise (middleware): refactore middleware to focus on better performance and security
This commit is contained in:
+3
-3
@@ -13,9 +13,9 @@
|
||||
<meta content="black-translucent" name="apple-mobile-web-app-status-bar-style">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/assets/logo/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/assets/logo/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/logo/favicon-16x16.png">
|
||||
<link rel="icon" href="/assets/logo/favicon.ico" type="image/x-icon" />
|
||||
<link rel="icon" type="image/png" href="/assets/logo/favicon-32x32.png" sizes="32x32">
|
||||
<link rel="icon" type="image/png" href="/assets/logo/favicon-16x16.png" sizes="16x16">
|
||||
<link rel="icon" type="image/x-icon" href="/assets/logo/favicon.ico" />
|
||||
<link rel="mask-icon" href="/assets/logo/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#2b5797">
|
||||
<meta name="theme-color" content="#f2f3f5">
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@
|
||||
"so": "application/octet-stream",
|
||||
"sr2": "image/x-sony-sr2",
|
||||
"srw": "image/x-samsung-srw",
|
||||
"svg": "image/svg",
|
||||
"svg": "image/svg+xml",
|
||||
"svgz": "image/svg+xml",
|
||||
"swf": "application/x-shockwave-flash",
|
||||
"tar": "application/x-tar",
|
||||
|
||||
+31
-11
@@ -141,16 +141,36 @@ func verify(something []byte) ([]byte, error) {
|
||||
// Create a unique ID that can be use to identify different session
|
||||
func GenerateID(ctx *App) string {
|
||||
params := ctx.Session
|
||||
p := "type =>" + params["type"]
|
||||
p += "salt => " + SECRET_KEY
|
||||
p += "host =>" + params["host"]
|
||||
p += "hostname =>" + params["hostname"]
|
||||
p += "username =>" + params["username"]
|
||||
p += "user =>" + params["user"]
|
||||
p += "repo =>" + params["repo"]
|
||||
p += "access_key_id =>" + params["access_key_id"]
|
||||
p += "endpoint =>" + params["endpoint"]
|
||||
p += "bearer =>" + params["bearer"]
|
||||
p += "token =>" + params["token"]
|
||||
p := "salt => " + SECRET_KEY
|
||||
if params["type"] != "" {
|
||||
p += "type =>" + params["type"]
|
||||
}
|
||||
if params["host"] != "" {
|
||||
p += "host =>" + params["host"]
|
||||
}
|
||||
if params["hostname"] != "" {
|
||||
p += "hostname =>" + params["hostname"]
|
||||
}
|
||||
if params["username"] != "" {
|
||||
p += "username =>" + params["username"]
|
||||
}
|
||||
if params["user"] != "" {
|
||||
p += "user =>" + params["user"]
|
||||
}
|
||||
if params["repo"] != "" {
|
||||
p += "repo =>" + params["repo"]
|
||||
}
|
||||
if params["access_key_id"] != "" {
|
||||
p += "access_key_id =>" + params["access_key_id"]
|
||||
}
|
||||
if params["endpoint"] != "" {
|
||||
p += "endpoint =>" + params["endpoint"]
|
||||
}
|
||||
if params["bearer"] != "" {
|
||||
p += "bearer =>" + params["bearer"]
|
||||
}
|
||||
if params["token"] != "" {
|
||||
p += "token =>" + params["token"]
|
||||
}
|
||||
return Hash(p)
|
||||
}
|
||||
|
||||
@@ -20,14 +20,14 @@ var HTTPClient = http.Client{
|
||||
}
|
||||
|
||||
var HTTP = http.Client{
|
||||
Timeout: 800 * time.Millisecond,
|
||||
Timeout: 10000 * time.Millisecond,
|
||||
Transport: &http.Transport{
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 500 * time.Millisecond,
|
||||
KeepAlive: 500 * time.Millisecond,
|
||||
Timeout: 5000 * time.Millisecond,
|
||||
KeepAlive: 5000 * time.Millisecond,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 500 * time.Millisecond,
|
||||
IdleConnTimeout: 500 * time.Millisecond,
|
||||
ResponseHeaderTimeout: 500 * time.Millisecond,
|
||||
TLSHandshakeTimeout: 5000 * time.Millisecond,
|
||||
IdleConnTimeout: 5000 * time.Millisecond,
|
||||
ResponseHeaderTimeout: 5000 * time.Millisecond,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package common
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetCurrentDir() string {
|
||||
@@ -20,3 +21,12 @@ func IsDirectory(path string) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func JoinPath(base, file string) (string, error) {
|
||||
filePath := filepath.Join(base, file)
|
||||
|
||||
if strings.HasPrefix(filePath, base) == false {
|
||||
return "", ErrNotValid
|
||||
}
|
||||
return filePath, nil
|
||||
}
|
||||
|
||||
@@ -21,25 +21,8 @@ func init(){
|
||||
logfile.WriteString("")
|
||||
}
|
||||
|
||||
type LogEntry struct {
|
||||
Host string `json:"host"`
|
||||
Method string `json:"method"`
|
||||
RequestURI string `json:"pathname"`
|
||||
Proto string `json:"proto"`
|
||||
Status int `json:"status"`
|
||||
Scheme string `json:"scheme"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
Ip string `json:"ip"`
|
||||
Referer string `json:"referer"`
|
||||
Timestamp time.Time `json:"_id"`
|
||||
Duration int64 `json:"responseTime"`
|
||||
Version string `json:"version"`
|
||||
Backend string `json:"backend"`
|
||||
}
|
||||
|
||||
type log struct{
|
||||
enable bool
|
||||
|
||||
debug bool
|
||||
info bool
|
||||
warn bool
|
||||
|
||||
+14
-2
@@ -30,10 +30,14 @@ func AdminSessionGet(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
token := AdminToken{}
|
||||
json.Unmarshal([]byte(str), &token)
|
||||
|
||||
if token.IsAdmin() == false || token.IsValid() == false {
|
||||
if token.IsValid() == false {
|
||||
SendSuccessResult(res, false)
|
||||
return
|
||||
} else if token.IsAdmin() == false {
|
||||
SendSuccessResult(res, false)
|
||||
return
|
||||
}
|
||||
|
||||
SendSuccessResult(res, true)
|
||||
}
|
||||
|
||||
@@ -75,10 +79,18 @@ func AdminSessionAuthenticate(ctx App, res http.ResponseWriter, req *http.Reques
|
||||
|
||||
func AdminBackend(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
backends := make(map[string]Form)
|
||||
|
||||
drivers := Backend.Drivers()
|
||||
for key := range drivers {
|
||||
backends[key] = drivers[key].LoginForm()
|
||||
}
|
||||
|
||||
if c, err := json.Marshal(backends); err == nil {
|
||||
hash := Hash(string(c))
|
||||
if req.Header.Get("If-None-Match") == hash {
|
||||
res.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
res.Header().Set("Etag", hash)
|
||||
}
|
||||
SendSuccessResult(res, backends)
|
||||
}
|
||||
|
||||
+11
-1
@@ -89,5 +89,15 @@ func PrivateConfigUpdateHandler(ctx App, res http.ResponseWriter, req *http.Requ
|
||||
}
|
||||
|
||||
func PublicConfigHandler(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
SendSuccessResult(res, Config.Export())
|
||||
cfg := Config.Export()
|
||||
|
||||
if c, err := Config.MarshalJSON(); err == nil {
|
||||
hash := Hash(string(c))
|
||||
if req.Header.Get("If-None-Match") == hash {
|
||||
res.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
res.Header().Set("Etag", hash)
|
||||
}
|
||||
SendSuccessResult(res, cfg)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package ctrl
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
. "github.com/mickael-kerjean/nuage/server/common"
|
||||
"github.com/mickael-kerjean/nuage/server/model"
|
||||
"io"
|
||||
@@ -78,6 +79,22 @@ func FileLs(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
perms.CanShare = NewBool(false)
|
||||
}
|
||||
|
||||
etag := func() string {
|
||||
tmp := struct {
|
||||
Tmp0 interface{}
|
||||
Tmp1 interface{}
|
||||
}{ files, perms }
|
||||
if j, err := json.Marshal(tmp); err == nil {
|
||||
return Hash(string(j))
|
||||
}
|
||||
return ""
|
||||
}()
|
||||
if etag != "" && req.Header.Get("If-None-Match") == etag {
|
||||
res.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
res.Header().Set("Etag", etag)
|
||||
|
||||
SendSuccessResultsWithMetadata(res, files, perms)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package ctrl
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/mickael-kerjean/mux"
|
||||
"github.com/gorilla/mux"
|
||||
. "github.com/mickael-kerjean/nuage/server/common"
|
||||
"github.com/mickael-kerjean/nuage/server/model"
|
||||
"net/http"
|
||||
|
||||
@@ -3,7 +3,7 @@ package ctrl
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/mickael-kerjean/mux"
|
||||
"github.com/gorilla/mux"
|
||||
. "github.com/mickael-kerjean/nuage/server/common"
|
||||
"github.com/mickael-kerjean/nuage/server/model"
|
||||
"net/http"
|
||||
|
||||
+90
-92
@@ -6,83 +6,46 @@ import (
|
||||
. "github.com/mickael-kerjean/nuage/server/common"
|
||||
"io"
|
||||
"text/template"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var ETAG_INDEX string
|
||||
var ETAGS map[string]string = make(map[string]string)
|
||||
|
||||
func StaticHandler(_path string, ctx App) http.Handler {
|
||||
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||
header := res.Header()
|
||||
header.Set("Content-Type", mime.TypeByExtension(filepath.Ext(req.URL.Path)))
|
||||
header.Set("Cache-Control", "max-age=2592000")
|
||||
header.Set("X-Content-Type-Options", "nosniff")
|
||||
func StaticHandler(_path string) func(App, http.ResponseWriter, *http.Request) {
|
||||
return func(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
var srcPath string
|
||||
var err error
|
||||
|
||||
if strings.HasSuffix(req.URL.Path, "/") {
|
||||
if srcPath, err = JoinPath(GetAbsolutePath(_path), req.URL.Path); err != nil {
|
||||
http.NotFound(res, req)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(_path, "/") == true {
|
||||
http.NotFound(res, req)
|
||||
return
|
||||
}
|
||||
|
||||
absPath := GetAbsolutePath(_path)
|
||||
fsrv := http.FileServer(http.Dir(absPath))
|
||||
_, err := os.Open(path.Join(absPath, req.URL.Path+".gz"))
|
||||
if err == nil && strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
|
||||
res.Header().Set("Content-Encoding", "gzip")
|
||||
req.URL.Path += ".gz"
|
||||
}
|
||||
fsrv.ServeHTTP(res, req)
|
||||
})
|
||||
ServeFile(res, req, srcPath)
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultHandler(_path string, ctx App) http.Handler {
|
||||
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||
_path := GetAbsolutePath(_path)
|
||||
|
||||
header := res.Header()
|
||||
header.Set("Content-Type", "text/html")
|
||||
header.Set("Cache-Control", "no-cache")
|
||||
header.Set("X-XSS-Protection", "1; mode=block")
|
||||
header.Set("X-Content-Type-Options", "nosniff")
|
||||
header.Set("X-Frame-Options", "DENY")
|
||||
if ETAG_INDEX == "" {
|
||||
ETAG_INDEX = hashFile(_path)
|
||||
}
|
||||
if req.Header.Get("If-None-Match") == ETAG_INDEX {
|
||||
res.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
header.Set("Etag", ETAG_INDEX)
|
||||
|
||||
|
||||
// Redirect to the admin section on first boot to setup the stuff
|
||||
func IndexHandler(_path string) func(App, http.ResponseWriter, *http.Request) {
|
||||
return func(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
if req.URL.String() != URL_SETUP && Config.Get("auth.admin").String() == "" {
|
||||
http.Redirect(res, req, URL_SETUP, http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := os.Open(_path+".gz"); err == nil && strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
|
||||
res.Header().Set("Content-Encoding", "gzip")
|
||||
_path += ".gz"
|
||||
}
|
||||
http.ServeFile(res, req, _path)
|
||||
})
|
||||
srcPath := GetAbsolutePath(_path)
|
||||
ServeFile(res, req, srcPath)
|
||||
}
|
||||
}
|
||||
|
||||
func AboutHandler(ctx App) http.Handler {
|
||||
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||
header := res.Header()
|
||||
header.Set("Content-Type", "text/html")
|
||||
header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
header.Set("X-XSS-Protection", "1; mode=block")
|
||||
header.Set("X-Content-Type-Options", "nosniff")
|
||||
header.Set("X-Frame-Options", "DENY")
|
||||
|
||||
page := `<!DOCTYPE html>
|
||||
func AboutHandler(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
page := `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
@@ -103,53 +66,88 @@ func AboutHandler(ctx App) http.Handler {
|
||||
</p>
|
||||
</body>
|
||||
</html>`
|
||||
t, _ := template.New("about").Parse(page)
|
||||
t.Execute(res, struct {
|
||||
App []string
|
||||
Plugins [][]string
|
||||
}{
|
||||
App: []string{"Nuage " + APP_VERSION, BUILD_NUMBER + "_" + hashFile(filepath.Join(GetCurrentDir(), "/nuage"))},
|
||||
Plugins: func () [][]string {
|
||||
pPath := filepath.Join(GetCurrentDir(), PLUGIN_PATH)
|
||||
file, err := os.Open(pPath)
|
||||
if err != nil {
|
||||
return [][]string{
|
||||
[]string{"N/A", ""},
|
||||
}
|
||||
t, _ := template.New("about").Parse(page)
|
||||
t.Execute(res, struct {
|
||||
App []string
|
||||
Plugins [][]string
|
||||
}{
|
||||
App: []string{"Nuage " + APP_VERSION, BUILD_NUMBER + "_" + hashFile(filepath.Join(GetCurrentDir(), "/nuage"), 6)},
|
||||
Plugins: func () [][]string {
|
||||
pPath := filepath.Join(GetCurrentDir(), PLUGIN_PATH)
|
||||
file, err := os.Open(pPath)
|
||||
if err != nil {
|
||||
return [][]string{
|
||||
[]string{"N/A", ""},
|
||||
}
|
||||
files, err := file.Readdir(0)
|
||||
if err != nil {
|
||||
return [][]string{
|
||||
[]string{"N/A", ""},
|
||||
}
|
||||
}
|
||||
files, err := file.Readdir(0)
|
||||
if err != nil {
|
||||
return [][]string{
|
||||
[]string{"N/A", ""},
|
||||
}
|
||||
plugins := make([][]string, 0)
|
||||
plugins = append(plugins, []string {
|
||||
"config.json",
|
||||
hashFile(filepath.Join(GetCurrentDir(), "/data/config/config.json")),
|
||||
}
|
||||
plugins := make([][]string, 0)
|
||||
plugins = append(plugins, []string {
|
||||
"config.json",
|
||||
hashFile(filepath.Join(GetCurrentDir(), "/data/config/config.json"), 6),
|
||||
})
|
||||
for i:=0; i < len(files); i++ {
|
||||
plugins = append(plugins, []string{
|
||||
files[i].Name(),
|
||||
hashFile(pPath + "/" + files[i].Name(), 6),
|
||||
})
|
||||
for i:=0; i < len(files); i++ {
|
||||
plugins = append(plugins, []string{
|
||||
files[i].Name(),
|
||||
hashFile(pPath + "/" + files[i].Name()),
|
||||
})
|
||||
}
|
||||
return plugins
|
||||
}(),
|
||||
})
|
||||
}
|
||||
return plugins
|
||||
}(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
func hashFile (path string) string {
|
||||
func hashFile (path string, n int) string {
|
||||
f, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
return "__"
|
||||
return ""
|
||||
}
|
||||
defer f.Close()
|
||||
h := md5.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "__"
|
||||
return ""
|
||||
}
|
||||
return base32.HexEncoding.EncodeToString(h.Sum(nil))[:6]
|
||||
return base32.HexEncoding.EncodeToString(h.Sum(nil))[:n]
|
||||
}
|
||||
|
||||
func ServeFile(res http.ResponseWriter, req *http.Request, filePath string) {
|
||||
zFilePath := filePath + ".gz"
|
||||
if req.Header.Get("If-None-Match") != "" {
|
||||
if req.Header.Get("If-None-Match") == ETAGS[filePath] {
|
||||
res.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
} else if req.Header.Get("If-None-Match") == ETAGS[zFilePath] {
|
||||
res.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
}
|
||||
head := res.Header()
|
||||
|
||||
if strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
|
||||
if file, err := os.OpenFile(zFilePath, os.O_RDONLY, os.ModePerm); err == nil {
|
||||
head.Set("Content-Encoding", "gzip")
|
||||
if ETAGS[zFilePath] == "" {
|
||||
ETAGS[zFilePath] = hashFile(zFilePath, 10)
|
||||
}
|
||||
head.Set("Etag", ETAGS[zFilePath])
|
||||
io.Copy(res, file)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(filePath, os.O_RDONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
http.NotFound(res, req)
|
||||
return
|
||||
}
|
||||
if ETAGS[filePath] == "" {
|
||||
ETAGS[filePath] = hashFile(filePath, 10)
|
||||
}
|
||||
head.Set("Etag", ETAGS[filePath])
|
||||
io.Copy(res, file)
|
||||
}
|
||||
|
||||
+3
-55
@@ -2,74 +2,22 @@ package ctrl
|
||||
|
||||
import (
|
||||
. "github.com/mickael-kerjean/nuage/server/common"
|
||||
. "github.com/mickael-kerjean/nuage/server/middleware"
|
||||
"github.com/mickael-kerjean/nuage/server/model"
|
||||
"github.com/mickael-kerjean/net/webdav"
|
||||
"github.com/mickael-kerjean/mux"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var start time.Time = time.Now()
|
||||
|
||||
func WebdavHandler(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
share_id := mux.Vars(req)["share"]
|
||||
prefix := "/s/" + share_id
|
||||
req.Header.Del("Content-Type")
|
||||
|
||||
if req.Method == "GET" && req.URL.Path == prefix {
|
||||
DefaultHandler(FILE_INDEX, ctx).ServeHTTP(res, req)
|
||||
return
|
||||
}
|
||||
|
||||
isCrap := func(p string) bool {
|
||||
if strings.HasPrefix(p, ".") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}(filepath.Base(req.URL.Path))
|
||||
if isCrap == true {
|
||||
if ctx.Share.Id == "" {
|
||||
http.NotFound(res, req)
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
if ctx.Share, err = ExtractShare(req, &ctx, share_id); err != nil {
|
||||
http.NotFound(res, req)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Session, err = ExtractSession(req, &ctx); err != nil {
|
||||
http.NotFound(res, req)
|
||||
return
|
||||
}
|
||||
if ctx.Backend, err = ExtractBackend(req, &ctx); err != nil || ctx.Backend == nil {
|
||||
http.NotFound(res, req)
|
||||
return
|
||||
}
|
||||
if share_id == "" {
|
||||
http.NotFound(res, req)
|
||||
return
|
||||
}
|
||||
|
||||
Log.Warning("==== REQUEST ('%s'): %s", req.Method, req.URL.Path)
|
||||
//start := time.Now()
|
||||
return
|
||||
h := &webdav.Handler{
|
||||
Prefix: "/s/" + share_id,
|
||||
Prefix: "/s/" + ctx.Share.Id,
|
||||
FileSystem: model.NewWebdavFs(ctx.Backend, ctx.Share.Path),
|
||||
LockSystem: webdav.NewMemLS(),
|
||||
Logger: func(r *http.Request, err error) {
|
||||
//Log.Info("==== REQUEST ('%s' => %d): %s\n", req.Method, time.Now().Sub(start) / (1000 * 1000), req.URL.Path)
|
||||
// e := func(err error) string{
|
||||
// if err != nil {
|
||||
// return err.Error()
|
||||
// }
|
||||
// return "OK"
|
||||
// }(err)
|
||||
//Log.Info("INFO %s WEBDAV %s %s %s", share_id, req.Method, req.URL.Path, e)
|
||||
},
|
||||
}
|
||||
h.ServeHTTP(res, req)
|
||||
}
|
||||
|
||||
+56
-39
@@ -1,7 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/mickael-kerjean/mux"
|
||||
"github.com/gorilla/mux"
|
||||
. "github.com/mickael-kerjean/nuage/server/common"
|
||||
. "github.com/mickael-kerjean/nuage/server/ctrl"
|
||||
. "github.com/mickael-kerjean/nuage/server/middleware"
|
||||
@@ -19,9 +19,10 @@ func main() {
|
||||
}
|
||||
|
||||
func Init(a *App) {
|
||||
var middlewares []Middleware
|
||||
r := mux.NewRouter()
|
||||
|
||||
// Profiling - handy to indentify nasty leaks and/or bugs!
|
||||
// Profiling - handy to identify nasty leaks and/or bugs!
|
||||
if os.Getenv("DEBUG") == "true" {
|
||||
r.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
@@ -38,49 +39,65 @@ func Init(a *App) {
|
||||
})
|
||||
}
|
||||
|
||||
// API
|
||||
// API for Session
|
||||
session := r.PathPrefix("/api/session").Subrouter()
|
||||
session.HandleFunc("", APIHandler(SessionGet, *a)).Methods("GET")
|
||||
session.HandleFunc("", APIHandler(SessionAuthenticate, *a)).Methods("POST")
|
||||
session.HandleFunc("", CtxInjector(SessionLogout, *a)).Methods("DELETE")
|
||||
session.Handle("/auth/{service}", APIHandler(SessionOAuthBackend, *a)).Methods("GET")
|
||||
middlewares = []Middleware{ ApiHeaders, SessionStart }
|
||||
session.HandleFunc("", NewMiddlewareChain(SessionGet, middlewares, *a)).Methods("GET")
|
||||
middlewares = []Middleware{ ApiHeaders, BodyParser }
|
||||
session.HandleFunc("", NewMiddlewareChain(SessionAuthenticate, middlewares, *a)).Methods("POST")
|
||||
middlewares = []Middleware{ ApiHeaders }
|
||||
session.HandleFunc("", NewMiddlewareChain(SessionLogout, middlewares, *a)).Methods("DELETE")
|
||||
session.HandleFunc("/auth/{service}", NewMiddlewareChain(SessionOAuthBackend, middlewares, *a)).Methods("GET")
|
||||
|
||||
files := r.PathPrefix("/api/files").Subrouter()
|
||||
files.HandleFunc("/ls", APIHandler(LoggedInOnly(FileLs), *a)).Methods("GET")
|
||||
files.HandleFunc("/cat", APIHandler(LoggedInOnly(FileCat), *a)).Methods("GET")
|
||||
files.HandleFunc("/cat", APIHandler(LoggedInOnly(FileSave), *a)).Methods("POST")
|
||||
files.HandleFunc("/mv", APIHandler(LoggedInOnly(FileMv), *a)).Methods("GET")
|
||||
files.HandleFunc("/rm", APIHandler(LoggedInOnly(FileRm), *a)).Methods("GET")
|
||||
files.HandleFunc("/mkdir", APIHandler(LoggedInOnly(FileMkdir), *a)).Methods("GET")
|
||||
files.HandleFunc("/touch", APIHandler(LoggedInOnly(FileTouch), *a)).Methods("GET")
|
||||
|
||||
share := r.PathPrefix("/api/share").Subrouter()
|
||||
share.HandleFunc("", APIHandler(ShareList, *a)).Methods("GET")
|
||||
share.HandleFunc("/{share}", APIHandler(ShareUpsert, *a)).Methods("POST")
|
||||
share.HandleFunc("/{share}", APIHandler(ShareDelete, *a)).Methods("DELETE")
|
||||
share.HandleFunc("/{share}/proof", APIHandler(ShareVerifyProof, *a)).Methods("POST")
|
||||
|
||||
// WEBDAV
|
||||
r.PathPrefix("/s/{share}").Handler(CtxInjector(WebdavHandler, *a))
|
||||
|
||||
// ADMIN
|
||||
// API for admin
|
||||
middlewares = []Middleware{ ApiHeaders }
|
||||
admin := r.PathPrefix("/admin/api").Subrouter()
|
||||
admin.HandleFunc("/session", CtxInjector(AdminSessionGet, *a)).Methods("GET")
|
||||
admin.HandleFunc("/session", CtxInjector(AdminSessionAuthenticate, *a)).Methods("POST")
|
||||
admin.HandleFunc("/plugin", CtxInjector(AdminOnly(FetchPluginsHandler), *a)).Methods("GET")
|
||||
admin.HandleFunc("/log", CtxInjector(AdminOnly(FetchLogHandler), *a)).Methods("GET")
|
||||
admin.HandleFunc("/config", CtxInjector(AdminOnly(PrivateConfigHandler), *a)).Methods("GET")
|
||||
admin.HandleFunc("/config", CtxInjector(AdminOnly(PrivateConfigUpdateHandler), *a)).Methods("POST")
|
||||
admin.HandleFunc("/session", NewMiddlewareChain(AdminSessionGet, middlewares, *a)).Methods("GET")
|
||||
admin.HandleFunc("/session", NewMiddlewareChain(AdminSessionAuthenticate, middlewares, *a)).Methods("POST")
|
||||
middlewares = []Middleware{ ApiHeaders, AdminOnly }
|
||||
admin.HandleFunc("/plugin", NewMiddlewareChain(FetchPluginsHandler, middlewares, *a)).Methods("GET")
|
||||
admin.HandleFunc("/log", NewMiddlewareChain(FetchLogHandler, middlewares, *a)).Methods("GET")
|
||||
admin.HandleFunc("/config", NewMiddlewareChain(PrivateConfigHandler, middlewares, *a)).Methods("GET")
|
||||
admin.HandleFunc("/config", NewMiddlewareChain(PrivateConfigUpdateHandler, middlewares, *a)).Methods("POST")
|
||||
|
||||
// APP
|
||||
r.HandleFunc("/api/config", CtxInjector(PublicConfigHandler, *a)).Methods("GET")
|
||||
r.HandleFunc("/api/backend", CtxInjector(AdminBackend, *a)).Methods("GET")
|
||||
// API for File management
|
||||
files := r.PathPrefix("/api/files").Subrouter()
|
||||
middlewares = []Middleware{ ApiHeaders, SessionStart, LoggedInOnly }
|
||||
files.HandleFunc("/ls", NewMiddlewareChain(FileLs, middlewares, *a)).Methods("GET")
|
||||
files.HandleFunc("/cat", NewMiddlewareChain(FileCat, middlewares, *a)).Methods("GET")
|
||||
files.HandleFunc("/cat", NewMiddlewareChain(FileSave, middlewares, *a)).Methods("POST")
|
||||
files.HandleFunc("/mv", NewMiddlewareChain(FileMv, middlewares, *a)).Methods("GET")
|
||||
files.HandleFunc("/rm", NewMiddlewareChain(FileRm, middlewares, *a)).Methods("GET")
|
||||
files.HandleFunc("/mkdir", NewMiddlewareChain(FileMkdir, middlewares, *a)).Methods("GET")
|
||||
files.HandleFunc("/touch", NewMiddlewareChain(FileTouch, middlewares, *a)).Methods("GET")
|
||||
|
||||
// API for Shared link
|
||||
share := r.PathPrefix("/api/share").Subrouter()
|
||||
middlewares = []Middleware{ ApiHeaders, SessionStart, LoggedInOnly }
|
||||
share.HandleFunc("", NewMiddlewareChain(ShareList, middlewares, *a)).Methods("GET")
|
||||
share.HandleFunc("/{share}", NewMiddlewareChain(ShareDelete, middlewares, *a)).Methods("DELETE")
|
||||
middlewares = []Middleware{ ApiHeaders, SessionStart, BodyParser, LoggedInOnly }
|
||||
share.HandleFunc("/{share}", NewMiddlewareChain(ShareUpsert, middlewares, *a)).Methods("POST")
|
||||
share.HandleFunc("/{share}/proof", NewMiddlewareChain(ShareVerifyProof, middlewares, *a)).Methods("POST")
|
||||
|
||||
// Webdav server / Shared Link
|
||||
middlewares = []Middleware{ IndexHeaders }
|
||||
r.HandleFunc("/s/{share}", NewMiddlewareChain(IndexHandler(FILE_INDEX), middlewares, *a)).Methods("GET")
|
||||
middlewares = []Middleware{ SessionStart }
|
||||
r.PathPrefix("/s/{share}").Handler(NewMiddlewareChain(WebdavHandler, middlewares, *a))
|
||||
|
||||
// Application Resources
|
||||
middlewares = []Middleware{ ApiHeaders }
|
||||
r.HandleFunc("/api/config", NewMiddlewareChain(PublicConfigHandler, middlewares, *a)).Methods("GET")
|
||||
r.HandleFunc("/api/backend", NewMiddlewareChain(AdminBackend, middlewares, *a)).Methods("GET")
|
||||
middlewares = []Middleware{ StaticHeaders }
|
||||
r.PathPrefix("/assets").Handler(http.HandlerFunc(NewMiddlewareChain(StaticHandler(FILE_ASSETS), middlewares, *a))).Methods("GET")
|
||||
r.HandleFunc("/favicon.ico", func(res http.ResponseWriter, req *http.Request) {
|
||||
http.Redirect(res, req, "/assets/logo/favicon.ico", http.StatusPermanentRedirect)
|
||||
http.Redirect(res, req, "/assets/logo/favicon.ico", http.StatusMovedPermanently)
|
||||
})
|
||||
r.PathPrefix("/assets").Handler(StaticHandler(FILE_ASSETS, *a)).Methods("GET")
|
||||
r.PathPrefix("/about").Handler(AboutHandler(*a)).Methods("GET")
|
||||
r.PathPrefix("/").Handler(DefaultHandler(FILE_INDEX, *a)).Methods("GET")
|
||||
middlewares = []Middleware{ IndexHeaders }
|
||||
r.HandleFunc("/about", NewMiddlewareChain(AboutHandler, middlewares, *a)).Methods("GET")
|
||||
r.PathPrefix("/").Handler(http.HandlerFunc(NewMiddlewareChain(IndexHandler(FILE_INDEX), middlewares, *a))).Methods("GET")
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: ":" + strconv.Itoa(Config.Get("general.port").Int()),
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
. "github.com/mickael-kerjean/nuage/server/common"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func LoggedInOnly(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 ctx.Backend == nil || ctx.Session == nil {
|
||||
SendErrorResult(res, NewError("Forbidden", 403))
|
||||
return
|
||||
}
|
||||
|
||||
fn(ctx, res, req)
|
||||
}
|
||||
}
|
||||
|
||||
func AdminOnly(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 admin := Config.Get("auth.admin").String(); admin != "" {
|
||||
c, err := req.Cookie(COOKIE_NAME_ADMIN);
|
||||
if err != nil {
|
||||
SendErrorResult(res, ErrPermissionDenied)
|
||||
return
|
||||
}
|
||||
|
||||
str, err := DecryptString(SECRET_KEY, c.Value);
|
||||
if err != nil {
|
||||
SendErrorResult(res, ErrPermissionDenied)
|
||||
return
|
||||
}
|
||||
token := AdminToken{}
|
||||
json.Unmarshal([]byte(str), &token)
|
||||
|
||||
if token.IsValid() == false || token.IsAdmin() == false {
|
||||
SendErrorResult(res, ErrPermissionDenied)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fn(ctx, res, req)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,31 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
. "github.com/mickael-kerjean/nuage/server/common"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func CtxInjector(fn func(App, http.ResponseWriter, *http.Request), ctx App) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||
func BodyParser (fn func(App, http.ResponseWriter, *http.Request)) func(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
extractBody := func(req *http.Request) (map[string]interface{}, error) {
|
||||
var body map[string]interface{}
|
||||
byt, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return body, err
|
||||
}
|
||||
if err := json.Unmarshal(byt, &body); err != nil {
|
||||
return body, err
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
return func(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
var err error
|
||||
if ctx.Body, err = extractBody(req); err != nil {
|
||||
SendErrorResult(res, ErrNotValid)
|
||||
return
|
||||
}
|
||||
fn(ctx, res, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+27
-181
@@ -1,197 +1,43 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"github.com/mickael-kerjean/nuage/server/model"
|
||||
. "github.com/mickael-kerjean/nuage/server/common"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func APIHandler(fn func(App, http.ResponseWriter, *http.Request), ctx App) http.HandlerFunc {
|
||||
return func(res http.ResponseWriter, req *http.Request) {
|
||||
var err error
|
||||
start := time.Now()
|
||||
|
||||
func ApiHeaders (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) {
|
||||
header := res.Header()
|
||||
header.Add("Content-Type", "application/json")
|
||||
if ctx.Body, err = ExtractBody(req); err != nil {
|
||||
SendErrorResult(res, ErrNotValid)
|
||||
return
|
||||
}
|
||||
share_id := req.URL.Query().Get("share");
|
||||
if ctx.Share, err = ExtractShare(req, &ctx, share_id); err != nil {
|
||||
SendErrorResult(res, err)
|
||||
return
|
||||
}
|
||||
if ctx.Session, err = ExtractSession(req, &ctx); err != nil {
|
||||
SendErrorResult(res, err)
|
||||
return
|
||||
}
|
||||
if ctx.Backend, err = ExtractBackend(req, &ctx); err != nil {
|
||||
SendErrorResult(res, err)
|
||||
return
|
||||
}
|
||||
resw := ResponseWriter{ResponseWriter: res}
|
||||
fn(ctx, &resw, req)
|
||||
req.Body.Close()
|
||||
|
||||
go func() {
|
||||
if Config.Get("log.telemetry").Bool() {
|
||||
go telemetry(req, &resw, start, ctx.Session["type"])
|
||||
}
|
||||
if Config.Get("log.enable").Bool() {
|
||||
go logger(req, &resw, start)
|
||||
}
|
||||
}()
|
||||
header.Set("Content-Type", "application/json")
|
||||
header.Set("Cache-Control", "no-cache")
|
||||
header.Set("X-Content-Type-Options", "nosniff")
|
||||
header.Set("X-XSS-Protection", "1; mode=block")
|
||||
header.Set("X-Frame-Options", "DENY")
|
||||
fn(ctx, res, req)
|
||||
}
|
||||
}
|
||||
|
||||
func ExtractBody(req *http.Request) (map[string]interface{}, error) {
|
||||
var body map[string]interface{}
|
||||
|
||||
if req.Method != "POST" {
|
||||
return body, nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(req.Header.Get("Content-Type"), "multipart/form-data") {
|
||||
return body, nil
|
||||
}
|
||||
byt, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return body, err
|
||||
}
|
||||
if err := json.Unmarshal(byt, &body); err != nil {
|
||||
return body, err
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func ExtractShare(req *http.Request, ctx *App, share_id string) (Share, error) {
|
||||
if share_id == "" {
|
||||
return Share{}, nil
|
||||
}
|
||||
|
||||
if Config.Get("features.share.enable").Bool() == false {
|
||||
Log.Debug("Share feature isn't enable, contact your administrator")
|
||||
return Share{}, NewError("Feature isn't enable, contact your administrator", 405)
|
||||
}
|
||||
|
||||
s, err := model.ShareGet(share_id)
|
||||
if err != nil {
|
||||
return Share{}, nil
|
||||
}
|
||||
if err = s.IsValid(); err != nil {
|
||||
return Share{}, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func ExtractSession(req *http.Request, ctx *App) (map[string]string, error) {
|
||||
var str string
|
||||
var res map[string]string = make(map[string]string)
|
||||
|
||||
if ctx.Share.Id != "" {
|
||||
var verifiedProof []model.Proof = model.ShareProofGetAlreadyVerified(req, ctx)
|
||||
var requiredProof []model.Proof = model.ShareProofGetRequired(ctx.Share)
|
||||
var remainingProof []model.Proof = model.ShareProofCalculateRemainings(requiredProof, verifiedProof)
|
||||
if len(remainingProof) != 0 {
|
||||
return res, NewError("Unauthorized Shared space", 400)
|
||||
}
|
||||
str = ctx.Share.Auth
|
||||
str, _ = DecryptString(SECRET_KEY, str)
|
||||
err := json.Unmarshal([]byte(str), &res)
|
||||
|
||||
if ctx.Share.Path[len(ctx.Share.Path)-1:] == "/" {
|
||||
res["path"] = ctx.Share.Path
|
||||
} else {
|
||||
path := req.URL.Query().Get("path")
|
||||
if strings.HasSuffix(ctx.Share.Path, path) == false {
|
||||
return res, ErrPermissionDenied
|
||||
}
|
||||
res["path"] = strings.TrimSuffix(ctx.Share.Path, path) + "/"
|
||||
}
|
||||
return res, err
|
||||
} else {
|
||||
cookie, err := req.Cookie(COOKIE_NAME_AUTH)
|
||||
if err != nil {
|
||||
return res, nil
|
||||
}
|
||||
str = cookie.Value
|
||||
str, err = DecryptString(SECRET_KEY, str)
|
||||
if err != nil {
|
||||
// This typically happen when changing the secret key
|
||||
return res, nil
|
||||
}
|
||||
err = json.Unmarshal([]byte(str), &res)
|
||||
return res, err
|
||||
func StaticHeaders (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) {
|
||||
header := res.Header()
|
||||
header.Set("Content-Type", GetMimeType(filepath.Ext(req.URL.Path)))
|
||||
header.Set("Cache-Control", "max-age=2592000")
|
||||
header.Set("X-Content-Type-Options", "nosniff")
|
||||
header.Set("X-XSS-Protection", "1; mode=block")
|
||||
header.Set("X-Frame-Options", "DENY")
|
||||
fn(ctx, res, req)
|
||||
}
|
||||
}
|
||||
|
||||
func ExtractBackend(req *http.Request, ctx *App) (IBackend, error) {
|
||||
return model.NewBackend(ctx, ctx.Session)
|
||||
}
|
||||
|
||||
func telemetry(req *http.Request, res *ResponseWriter, start time.Time, backendType string) {
|
||||
point := logPoint(req, res, start, backendType)
|
||||
body, err := json.Marshal(point)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
formData := bytes.NewReader(body)
|
||||
|
||||
r, err := http.NewRequest("POST", "https://log.kerjean.me/nuage", formData)
|
||||
r.Header.Set("Connection", "Close")
|
||||
r.Close = true
|
||||
if err != nil {
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
resp, err := HTTP.Do(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
func logger(req *http.Request, res *ResponseWriter, start time.Time) {
|
||||
point := logPoint(req, res, start, "")
|
||||
Log.Info("HTTP %s %3d %03dms %s", point.Method, point.Status, point.Duration, point.RequestURI)
|
||||
}
|
||||
|
||||
func logPoint(req *http.Request, res *ResponseWriter, start time.Time, backendType string) *LogEntry {
|
||||
return &LogEntry{
|
||||
Version: APP_VERSION,
|
||||
Scheme: req.URL.Scheme,
|
||||
Host: req.Host,
|
||||
Method: req.Method,
|
||||
RequestURI: req.RequestURI,
|
||||
Proto: req.Proto,
|
||||
Status: res.status,
|
||||
UserAgent: req.Header.Get("User-Agent"),
|
||||
Ip: req.RemoteAddr,
|
||||
Referer: req.Referer(),
|
||||
Duration: int64(time.Now().Sub(start) / (1000 * 1000)),
|
||||
Timestamp: time.Now().UTC(),
|
||||
Backend: backendType,
|
||||
func IndexHeaders (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) {
|
||||
header := res.Header()
|
||||
header.Set("Content-Type", "text/html")
|
||||
header.Set("Cache-Control", "no-cache")
|
||||
header.Set("X-Content-Type-Options", "nosniff")
|
||||
header.Set("X-XSS-Protection", "1; mode=block")
|
||||
header.Set("X-Frame-Options", "DENY")
|
||||
fn(ctx, res, req)
|
||||
}
|
||||
}
|
||||
|
||||
type ResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
}
|
||||
|
||||
func (w *ResponseWriter) WriteHeader(status int) {
|
||||
w.status = status
|
||||
w.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
|
||||
func (w *ResponseWriter) Write(b []byte) (int, error) {
|
||||
if w.status == 0 {
|
||||
w.status = 200
|
||||
}
|
||||
return w.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
. "github.com/mickael-kerjean/nuage/server/common"
|
||||
"time"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Middleware func(func(App, http.ResponseWriter, *http.Request)) func(App, http.ResponseWriter, *http.Request)
|
||||
|
||||
func NewMiddlewareChain(fn func(App, http.ResponseWriter, *http.Request), m []Middleware, app App) http.HandlerFunc {
|
||||
return func(res http.ResponseWriter, req *http.Request) {
|
||||
var resw ResponseWriter = NewResponseWriter(res)
|
||||
var f func(App, http.ResponseWriter, *http.Request) = fn
|
||||
|
||||
for i := len(m) - 1; i >= 0; i-- {
|
||||
f = m[i](f)
|
||||
}
|
||||
f(app, &resw, req)
|
||||
req.Body.Close()
|
||||
go Logger(app, &resw, req)
|
||||
}
|
||||
}
|
||||
|
||||
type ResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
start time.Time
|
||||
}
|
||||
|
||||
func NewResponseWriter(res http.ResponseWriter) ResponseWriter {
|
||||
return ResponseWriter{
|
||||
ResponseWriter: res,
|
||||
start: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ResponseWriter) WriteHeader(status int) {
|
||||
w.status = status
|
||||
w.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
|
||||
func (w *ResponseWriter) Write(b []byte) (int, error) {
|
||||
if w.status == 0 {
|
||||
w.status = 200
|
||||
}
|
||||
return w.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
type LogEntry struct {
|
||||
Host string `json:"host"`
|
||||
Method string `json:"method"`
|
||||
RequestURI string `json:"pathname"`
|
||||
Proto string `json:"proto"`
|
||||
Status int `json:"status"`
|
||||
Scheme string `json:"scheme"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
Ip string `json:"ip"`
|
||||
Referer string `json:"referer"`
|
||||
Timestamp time.Time `json:"_id"`
|
||||
Duration float64 `json:"responseTime"`
|
||||
Version string `json:"version"`
|
||||
Backend string `json:"backend"`
|
||||
}
|
||||
|
||||
func Logger(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
if obj, ok := res.(*ResponseWriter); ok {
|
||||
point := LogEntry{
|
||||
Version: APP_VERSION,
|
||||
Scheme: req.URL.Scheme,
|
||||
Host: req.Host,
|
||||
Method: req.Method,
|
||||
RequestURI: req.RequestURI,
|
||||
Proto: req.Proto,
|
||||
Status: obj.status,
|
||||
UserAgent: req.Header.Get("User-Agent"),
|
||||
Ip: req.RemoteAddr,
|
||||
Referer: req.Referer(),
|
||||
Duration: float64(time.Now().Sub(obj.start)) / (1000 * 1000),
|
||||
Timestamp: time.Now().UTC(),
|
||||
Backend: ctx.Session["type"],
|
||||
}
|
||||
if Config.Get("log.telemetry").Bool() {
|
||||
telemetry.Record(point)
|
||||
}
|
||||
if Config.Get("log.enable").Bool() {
|
||||
Log.Info("HTTP %3d %3s %6.1fms %s", point.Status, point.Method, point.Duration, point.RequestURI)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Telemetry struct {
|
||||
data []LogEntry
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (this *Telemetry) Record(point LogEntry) {
|
||||
this.mu.Lock()
|
||||
this.data = append(this.data, point)
|
||||
this.mu.Unlock()
|
||||
}
|
||||
|
||||
func (this *Telemetry) Flush() {
|
||||
if len(this.data) == 0 {
|
||||
return
|
||||
}
|
||||
this.mu.Lock()
|
||||
pts := this.data
|
||||
this.data = make([]LogEntry, 0)
|
||||
this.mu.Unlock()
|
||||
|
||||
// send data in bulk: http://docs.couchdb.org/en/2.2.0/api/database/bulk-api.html#inserting-documents-in-bulk
|
||||
data := struct {
|
||||
Docs []LogEntry `json:"docs"`
|
||||
}{ pts }
|
||||
body, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
r, err := http.NewRequest("POST", "https://log.kerjean.me/trash/_bulk_docs", bytes.NewReader(body))
|
||||
r.Header.Set("Connection", "Close")
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
r.Close = true
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp, err := HTTP.Do(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
var telemetry Telemetry = Telemetry{ data: make([]LogEntry, 0) }
|
||||
|
||||
func init(){
|
||||
go func(){
|
||||
for {
|
||||
select {
|
||||
default:
|
||||
time.Sleep(1000 * 10 * time.Millisecond)
|
||||
telemetry.Flush()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
. "github.com/mickael-kerjean/nuage/server/common"
|
||||
"github.com/mickael-kerjean/nuage/server/model"
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func LoggedInOnly(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 ctx.Backend == nil || ctx.Session == nil {
|
||||
SendErrorResult(res, ErrPermissionDenied)
|
||||
return
|
||||
}
|
||||
fn(ctx, res, req)
|
||||
}
|
||||
}
|
||||
|
||||
func AdminOnly(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 admin := Config.Get("auth.admin").String(); admin != "" {
|
||||
c, err := req.Cookie(COOKIE_NAME_ADMIN);
|
||||
if err != nil {
|
||||
SendErrorResult(res, ErrPermissionDenied)
|
||||
return
|
||||
}
|
||||
|
||||
str, err := DecryptString(SECRET_KEY, c.Value);
|
||||
if err != nil {
|
||||
SendErrorResult(res, ErrPermissionDenied)
|
||||
return
|
||||
}
|
||||
token := AdminToken{}
|
||||
json.Unmarshal([]byte(str), &token)
|
||||
|
||||
if token.IsValid() == false || token.IsAdmin() == false {
|
||||
SendErrorResult(res, ErrPermissionDenied)
|
||||
return
|
||||
}
|
||||
}
|
||||
fn(ctx, res, req)
|
||||
}
|
||||
}
|
||||
|
||||
func SessionStart (fn func(App, http.ResponseWriter, *http.Request)) func(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
extractShare := func(req *http.Request, ctx *App, share_id string) (Share, error) {
|
||||
if share_id == "" {
|
||||
return Share{}, nil
|
||||
}
|
||||
|
||||
if Config.Get("features.share.enable").Bool() == false {
|
||||
Log.Debug("Share feature isn't enable, contact your administrator")
|
||||
return Share{}, NewError("Feature isn't enable, contact your administrator", 405)
|
||||
}
|
||||
|
||||
s, err := model.ShareGet(share_id)
|
||||
if err != nil {
|
||||
return Share{}, nil
|
||||
}
|
||||
if err = s.IsValid(); err != nil {
|
||||
return Share{}, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
extractSession := func(req *http.Request, ctx *App) (map[string]string, error) {
|
||||
var str string
|
||||
var err error
|
||||
var res map[string]string = make(map[string]string)
|
||||
|
||||
if ctx.Share.Id != "" {
|
||||
var verifiedProof []model.Proof = model.ShareProofGetAlreadyVerified(req, ctx)
|
||||
var requiredProof []model.Proof = model.ShareProofGetRequired(ctx.Share)
|
||||
var remainingProof []model.Proof = model.ShareProofCalculateRemainings(requiredProof, verifiedProof)
|
||||
if len(remainingProof) != 0 {
|
||||
return res, NewError("Unauthorized Shared space", 400)
|
||||
}
|
||||
str = ctx.Share.Auth
|
||||
str, err = DecryptString(SECRET_KEY, str)
|
||||
if err != nil {
|
||||
// This typically happen when changing the secret key
|
||||
return res, nil
|
||||
}
|
||||
err = json.Unmarshal([]byte(str), &res)
|
||||
|
||||
if ctx.Share.Path[len(ctx.Share.Path)-1:] == "/" {
|
||||
res["path"] = ctx.Share.Path
|
||||
} else {
|
||||
path := req.URL.Query().Get("path")
|
||||
if strings.HasSuffix(ctx.Share.Path, path) == false {
|
||||
return res, ErrPermissionDenied
|
||||
}
|
||||
res["path"] = strings.TrimSuffix(ctx.Share.Path, path) + "/"
|
||||
}
|
||||
return res, err
|
||||
} else {
|
||||
cookie, err := req.Cookie(COOKIE_NAME_AUTH)
|
||||
if err != nil {
|
||||
return res, nil
|
||||
}
|
||||
str = cookie.Value
|
||||
str, err = DecryptString(SECRET_KEY, str)
|
||||
if err != nil {
|
||||
// This typically happen when changing the secret key
|
||||
return res, nil
|
||||
}
|
||||
err = json.Unmarshal([]byte(str), &res)
|
||||
return res, err
|
||||
}
|
||||
}
|
||||
extractBackend := func(req *http.Request, ctx *App) (IBackend, error) {
|
||||
return model.NewBackend(ctx, ctx.Session)
|
||||
}
|
||||
|
||||
return func(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
var err error
|
||||
share_id := func() string {
|
||||
if len(req.URL.Path) > 3 && req.URL.Path[:3] == "/s/" {
|
||||
// this runs while using a link as a webdav server
|
||||
return mux.Vars(req)["share"]
|
||||
}
|
||||
return req.URL.Query().Get("share")
|
||||
}()
|
||||
if ctx.Share, err = extractShare(req, &ctx, share_id); err != nil {
|
||||
SendErrorResult(res, err)
|
||||
return
|
||||
}
|
||||
if ctx.Session, err = extractSession(req, &ctx); err != nil {
|
||||
SendErrorResult(res, err)
|
||||
return
|
||||
}
|
||||
if ctx.Backend, err = extractBackend(req, &ctx); err != nil {
|
||||
SendErrorResult(res, err)
|
||||
return
|
||||
}
|
||||
fn(ctx, res, req)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user