diff --git a/go.mod b/go.mod index eeba8f276..434a54514 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.13 require ( github.com/ProtonMail/go-appdir v1.0.0 github.com/PuerkitoBio/goquery v1.5.0 + github.com/abbot/go-http-auth v0.4.0 github.com/anacrolix/envpprof v1.0.0 // indirect github.com/anacrolix/ffprobe v0.0.0-20190307025918-9b483c5f7751 github.com/anacrolix/missinggo v1.1.0 // indirect @@ -84,6 +85,7 @@ require ( github.com/x-cray/logrus-prefixed-formatter v0.5.2 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect go.etcd.io/bbolt v1.3.3 // indirect + golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 golang.org/x/net v0.0.0-20191112182307-2180aed22343 golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914 golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea diff --git a/pkg/xbvr/api_deovr.go b/pkg/xbvr/api_deovr.go index 798f2e28a..9835feca8 100644 --- a/pkg/xbvr/api_deovr.go +++ b/pkg/xbvr/api_deovr.go @@ -4,6 +4,7 @@ import ( "fmt" "net" "net/http" + "net/http/httputil" "os" "strings" @@ -15,7 +16,7 @@ import ( type DeoLibrary struct { Scenes []DeoListScenes `json:"scenes"` - Authorized int `json:"authorized"` + Authorized string `json:"authorized"` } type DeoListScenes struct { @@ -69,6 +70,56 @@ type DeoSceneVideoSource struct { URL string `json:"url"` } +func deoAuthEnabled() bool { + if DEOPASSWORD != "" && DEOUSER != "" { + return true + } else { + return false + } +} + +func restfulAuthFilter(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) { + // Save a copy of this request for debugging. + requestDump, err := httputil.DumpRequest(req.Request, true) + if err != nil { + log.Error(err) + } + log.Infoln("HTTP Request from DeoVR:\n\n" + string(requestDump)) + + if deoAuthEnabled() { + var authorized bool + + u, err := req.BodyParameter("login") + if err != nil { + authorized = false + } + + p, err := req.BodyParameter("password") + if err != nil { + authorized = false + } + + if u == DEOUSER && p == DEOPASSWORD { + authorized = true + } + + if !authorized { + unauthLib := DeoLibrary{ + Authorized: "-1", + Scenes: []DeoListScenes{ + { + Name: "Login Required", + List: nil, + }, + }, + } + resp.WriteHeaderAndEntity(http.StatusOK, unauthLib) + return + } + } + chain.ProcessFilter(req, resp) +} + type DeoVRResource struct{} func (i DeoVRResource) WebService() *restful.WebService { @@ -77,14 +128,20 @@ func (i DeoVRResource) WebService() *restful.WebService { ws := new(restful.WebService) ws.Path("/deovr/"). - Consumes(restful.MIME_JSON). + Consumes(restful.MIME_JSON, "application/x-www-form-urlencoded"). Produces(restful.MIME_JSON) - ws.Route(ws.GET("").To(i.getDeoLibrary). + ws.Route(ws.GET("").Filter(restfulAuthFilter).To(i.getDeoLibrary). + Metadata(restfulspec.KeyOpenAPITags, tags). + Writes(DeoLibrary{})) + ws.Route(ws.POST("").Filter(restfulAuthFilter).To(i.getDeoLibrary). Metadata(restfulspec.KeyOpenAPITags, tags). Writes(DeoLibrary{})) - ws.Route(ws.GET("/{scene-id}").To(i.getDeoScene). + ws.Route(ws.GET("/{scene-id}").Filter(restfulAuthFilter).To(i.getDeoScene). + Metadata(restfulspec.KeyOpenAPITags, tags). + Writes(DeoScene{})) + ws.Route(ws.POST("/{scene-id}").Filter(restfulAuthFilter).To(i.getDeoScene). Metadata(restfulspec.KeyOpenAPITags, tags). Writes(DeoScene{})) @@ -198,7 +255,7 @@ func (i DeoVRResource) getDeoLibrary(req *restful.Request, resp *restful.Respons Find(&watchlist) lib := DeoLibrary{ - Authorized: 1, + Authorized: "1", Scenes: []DeoListScenes{ { Name: "Recent releases", diff --git a/pkg/xbvr/server.go b/pkg/xbvr/server.go index bf996666c..c057bb83b 100644 --- a/pkg/xbvr/server.go +++ b/pkg/xbvr/server.go @@ -5,9 +5,13 @@ import ( "net" "net/http" "net/url" + "os" "path/filepath" "strings" + "golang.org/x/crypto/bcrypt" + + auth "github.com/abbot/go-http-auth" "github.com/emicklei/go-restful" restfulspec "github.com/emicklei/go-restful-openapi" "github.com/gammazero/nexus/v3/router" @@ -28,12 +32,46 @@ import ( var ( DEBUG = common.DEBUG + DEOPASSWORD = os.Getenv("DEO_PASSWORD") + DEOUSER = os.Getenv("DEO_USERNAME") + UIPASSWORD = os.Getenv("UI_PASSWORD") + UIUSER = os.Getenv("UI_USERNAME") DLNA = common.DLNA httpAddr = common.HttpAddr wsAddr = common.WsAddr currentVersion = "" ) +func uiAuthEnabled() bool { + if UIPASSWORD != "" && UIUSER != "" { + return true + } else { + return false + } +} + +func uiSecret(user string, realm string) string { + if user == UIUSER { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(UIPASSWORD), bcrypt.DefaultCost) + if err == nil { + return string(hashedPassword) + } + } + return "" +} + +func authHandle(pattern string, authEnabled bool, authSecret auth.SecretProvider, handler http.Handler) { + if authEnabled { + authenticator := auth.NewBasicAuthenticator("default", authSecret) + http.HandleFunc(pattern, authenticator.Wrap(func(res http.ResponseWriter, req *auth.AuthenticatedRequest) { + http.StripPrefix(pattern, handler).ServeHTTP(res, &req.Request) + })) + } else { + http.Handle(pattern, http.StripPrefix(pattern, handler)) + } + +} + func StartServer(version, commit, branch, date string) { currentVersion = version @@ -101,9 +139,9 @@ func StartServer(version, commit, branch, date string) { // Static files if DEBUG == "" { - http.Handle("/ui/", http.StripPrefix("/ui", http.FileServer(assets.HTTP))) + authHandle("/ui/", uiAuthEnabled(), uiSecret, http.FileServer(assets.HTTP)) } else { - http.Handle("/ui/", http.StripPrefix("/ui", http.FileServer(http.Dir("ui/dist")))) + authHandle("/ui/", uiAuthEnabled(), uiSecret, http.FileServer(http.Dir("ui/dist"))) } // Imageproxy @@ -181,6 +219,8 @@ func StartServer(version, commit, branch, date string) { } } log.Infof("Web UI available at %s", strings.Join(ips, ", ")) + log.Infof("Web UI Authentication enabled: %v", uiAuthEnabled()) + log.Infof("DeoVR Authentication enabled: %v", deoAuthEnabled()) log.Infof("Database file stored at %s", common.AppDir) if DEBUG == "" {