optimise (middleware): refactore middleware to focus on better performance and security

This commit is contained in:
Mickael KERJEAN
2019-01-28 01:09:45 +11:00
parent a50dbd4724
commit ab93520773
19 changed files with 582 additions and 458 deletions
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+6 -6
View File
@@ -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,
},
}
+10
View File
@@ -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
}
-17
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+17
View File
@@ -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)
}
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()),
-45
View File
@@ -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)
}
}
+22 -3
View File
@@ -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
View File
@@ -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)
}
+149
View File
@@ -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()
}
}
}()
}
+140
View File
@@ -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)
}
}