mirror of
				https://github.com/mickael-kerjean/filestash.git
				synced 2025-10-31 18:16:00 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			171 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			171 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
|  * 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)
 | |
| }
 | 
