diff --git a/server/main.go b/server/main.go index 85393958..cf124409 100644 --- a/server/main.go +++ b/server/main.go @@ -44,8 +44,7 @@ func Init(a *App) { admin.HandleFunc("/config", NewMiddlewareChain(PrivateConfigHandler, middlewares, *a)).Methods("GET") admin.HandleFunc("/config", NewMiddlewareChain(PrivateConfigUpdateHandler, middlewares, *a)).Methods("POST") middlewares = []Middleware{ IndexHeaders } - admin.HandleFunc("/log", NewMiddlewareChain(FetchLogHandler, middlewares, *a)).Methods("GET") - r.PathPrefix("/admin").Handler(http.HandlerFunc(NewMiddlewareChain(IndexHandler(FILE_INDEX), middlewares, *a))).Methods("GET") + admin.HandleFunc("/log", NewMiddlewareChain(FetchLogHandler, middlewares, *a)).Methods("GET") // API for File management files := r.PathPrefix("/api/files").Subrouter() @@ -106,7 +105,8 @@ func Init(a *App) { } initPluginsRoutes(r, a) - r.PathPrefix("/").Handler(http.HandlerFunc(NewMiddlewareChain(IndexHandler(FILE_INDEX), middlewares, *a))).Methods("GET") + r.PathPrefix("/admin").Handler(http.HandlerFunc(NewMiddlewareChain(IndexHandler(FILE_INDEX), middlewares, *a))).Methods("GET") + r.PathPrefix("/" ).Handler(http.HandlerFunc(NewMiddlewareChain(IndexHandler(FILE_INDEX), middlewares, *a))).Methods("GET") // Routes are served via plugins to avoid getting stuck with plain HTTP. The idea is to // support many more protocols in the future: HTTPS, HTTP2, TOR or whatever that sounds diff --git a/server/plugin/index.go b/server/plugin/index.go index 1b99f929..ac10dd44 100644 --- a/server/plugin/index.go +++ b/server/plugin/index.go @@ -5,6 +5,7 @@ import ( _ "github.com/mickael-kerjean/filestash/server/plugin/plg_starter_tor" _ "github.com/mickael-kerjean/filestash/server/plugin/plg_video_transcoder" _ "github.com/mickael-kerjean/filestash/server/plugin/plg_editor_onlyoffice" + _ "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_syncthing" _ "github.com/mickael-kerjean/filestash/server/plugin/plg_image_light" _ "github.com/mickael-kerjean/filestash/server/plugin/plg_backend_backblaze" _ "github.com/mickael-kerjean/filestash/server/plugin/plg_backend_dav" diff --git a/server/plugin/plg_handler_syncthing/index.go b/server/plugin/plg_handler_syncthing/index.go new file mode 100644 index 00000000..c6040b6f --- /dev/null +++ b/server/plugin/plg_handler_syncthing/index.go @@ -0,0 +1,161 @@ +/* + * This plugin expose syncthing to the admin user + */ +package plg_handler_syncthing + +import ( + "encoding/base64" + "fmt" + "github.com/gorilla/mux" + . "github.com/mickael-kerjean/filestash/server/common" + "golang.org/x/crypto/bcrypt" + "net/http" + "net/http/httputil" + "net/url" + "os" + "strings" + "time" +) + +const SYNCTHING_URI = "/admin/syncthing" +var syncthing_enable func() bool = func() bool { + return Config.Get("features.syncthing.enable").Schema(func(f *FormElement) *FormElement { + if f == nil { + f = &FormElement{} + } + f.Name = "enable" + f.Type = "enable" + f.Target = []string{"syncthing_server_url"} + f.Description = "Enable/Disable the office suite to manage word, excel and powerpoint documents. This setting requires a restart to comes into effect" + f.Default = false + if u := os.Getenv("SYNCTHING_URL"); u != "" { + f.Default = true + } + return f + }).Bool() +} +func init() { + syncthing_enable() + Config.Get("features.syncthing.server_url").Schema(func(f *FormElement) *FormElement { + if f == nil { + f = &FormElement{} + } + f.Id = "syncthing_server_url" + f.Name = "server_url" + f.Type = "text" + f.Description = "Location of your Syncthing server" + f.Default = "" + f.Placeholder = "Eg: http://127.0.0.1:8080" + if u := os.Getenv("SYNCTHING_URL"); u != "" { + f.Default = u + f.Placeholder = fmt.Sprintf("Default: '%s'", u) + } + return f + }) + + Hooks.Register.HttpEndpoint(func(r *mux.Router, _ *App) error { + r.HandleFunc(SYNCTHING_URI, func (res http.ResponseWriter, req *http.Request) { + http.Redirect(res, req, SYNCTHING_URI + "/", http.StatusTemporaryRedirect) + }) + r.Handle(SYNCTHING_URI + "/", AuthBasic( + func() (string, string) { return "admin", Config.Get("auth.admin").String() }, + http.HandlerFunc(SyncthingProxyHandler), + )) + + r.PathPrefix(SYNCTHING_URI + "/").HandlerFunc(SyncthingProxyHandler) + return nil + }) +} + +func AuthBasic(credentials func() (string, string), fn http.Handler) http.HandlerFunc { + var notAuthorised = func(res http.ResponseWriter, req *http.Request) { + time.Sleep(1 * time.Second) + res.Header().Set("WWW-Authenticate", `Basic realm="User protect", charset="UTF-8"`) + res.WriteHeader(http.StatusUnauthorized) + res.Write([]byte("Not Authorised")) + return + } + + return func(res http.ResponseWriter, req *http.Request) { + if syncthing_enable() == false { + http.NotFoundHandler().ServeHTTP(res, req) + return + } + + auth := req.Header.Get("Authorization") + if strings.HasPrefix(auth, "Basic ") == false { + notAuthorised(res, req) + return + } + auth = strings.TrimPrefix(auth, "Basic ") + decoded, err := base64.StdEncoding.DecodeString(auth) + if err != nil { + notAuthorised(res, req) + return + } + auth = string(decoded) + stuffs := strings.Split(auth, ":") + if len(stuffs) < 2 { + notAuthorised(res, req) + return + } + username := stuffs[0] + password := strings.Join(stuffs[1:], ":") + refUsername, refPassword := credentials() + if refUsername != username { + notAuthorised(res, req) + return + } else if err = bcrypt.CompareHashAndPassword([]byte(refPassword), []byte(password)); err != nil { + notAuthorised(res, req) + return + } + fn.ServeHTTP(res, req) + return + } +} + +func SyncthingProxyHandler(res http.ResponseWriter, req *http.Request) { + req.URL.Path = strings.TrimPrefix(req.URL.Path, SYNCTHING_URI) + req.Header.Set("X-Forwarded-Host", req.Host + SYNCTHING_URI) + req.Header.Set("X-Forwarded-Proto", func() string { + if scheme := req.Header.Get("X-Forwarded-Proto"); scheme != "" { + return scheme + } else if req.TLS != nil { + return "https" + } + return "http" + }()) + u, err := url.Parse(Config.Get("features.syncthing.server_url").String()) + if err != nil { + SendErrorResult(res, err) + return + } + + reverseProxy := &httputil.ReverseProxy{ + Director: func(rq *http.Request) { + rq.URL.Scheme = "http" + rq.URL.Host = u.Host + rq.URL.Path = func(a, b string) string { + aslash := strings.HasSuffix(a, "/") + bslash := strings.HasPrefix(b, "/") + switch { + case aslash && bslash: + return a + b[1:] + case !aslash && !bslash: + return a + "/" + b + } + return a + b + }(u.Path, rq.URL.Path) + if u.RawQuery == "" || rq.URL.RawQuery == "" { + rq.URL.RawQuery = u.RawQuery + rq.URL.RawQuery + } else { + rq.URL.RawQuery = u.RawQuery + "&" + rq.URL.RawQuery + } + }, + } + reverseProxy.ErrorHandler = func(rw http.ResponseWriter, rq *http.Request, err error) { + Log.Warning("[syncthing] %s", err.Error()) + SendErrorResult(rw, NewError(err.Error(), http.StatusBadGateway)) + } + reverseProxy.ServeHTTP(res, req) +}