/* * 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 ( plugin_enable func() bool server_url func() string ) func init() { plugin_enable = 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 integration with the syncthing server. This will make your syncthing server available at `/admin/syncthing`" f.Default = false if u := os.Getenv("SYNCTHING_URL"); u != "" { f.Default = true } return f }).Bool() } server_url = func() string { return 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 }).String() } Hooks.Register.Onload(func() { plugin_enable() server_url() }) Hooks.Register.HttpEndpoint(func(r *mux.Router, _ *App) error { if plugin_enable() == false { return nil } 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) { 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(server_url()) 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) }