diff --git a/frontend/src/common/notify.js b/frontend/src/common/notify.js index c75ada9b7..d0ad15ed1 100644 --- a/frontend/src/common/notify.js +++ b/frontend/src/common/notify.js @@ -15,7 +15,7 @@ const Notify = { }, logout: function (message) { Event.publish("notify.error", {msg: message}); - Event.publish("session.logout"); + Event.publish("session.logout", {msg: message}); }, ajaxStart: function() { Event.publish("ajax.start"); diff --git a/frontend/src/common/session.js b/frontend/src/common/session.js index 60d20fbb9..869d1f325 100644 --- a/frontend/src/common/session.js +++ b/frontend/src/common/session.js @@ -1,6 +1,7 @@ import Api from "./api"; import Event from "pubsub-js"; import User from "../model/user"; +import Socket from "./websocket"; export default class Session { /** @@ -24,7 +25,15 @@ export default class Session { this.auth = true; } - Event.subscribe("session.logout", this.onLogout.bind(this)); + Event.subscribe("session.logout", () => { + this.onLogout() + }); + + Event.subscribe("websocket.connected", () => { + this.sendClientInfo() + }); + + this.sendClientInfo() } useSessionStorage() { @@ -118,6 +127,21 @@ export default class Session { this.storage.removeItem("user"); } + sendClientInfo() { + const clientInfo = { + "session": this.getToken(), + "js": window.clientConfig.jsHash, + "css": window.clientConfig.cssHash, + "version": window.clientConfig.version, + }; + + try { + Socket.send(JSON.stringify(clientInfo)); + } catch(e) { + console.log("can't send client info, websocket not connected (yet)") + } + } + login(email, password) { this.deleteToken(); @@ -125,11 +149,13 @@ export default class Session { (result) => { this.setToken(result.data.token); this.setUser(new User(result.data.user)); + this.sendClientInfo(); } ); } onLogout() { + console.log("ON LOGOUT"); this.deleteToken(); window.location = "/"; } diff --git a/frontend/src/common/websocket.js b/frontend/src/common/websocket.js index 15b5b87f4..a4b2543d8 100644 --- a/frontend/src/common/websocket.js +++ b/frontend/src/common/websocket.js @@ -1,22 +1,15 @@ import Sockette from "sockette"; import Event from "pubsub-js"; -import randomString from "crypto-random-string"; -export const token = randomString({length: 16}); const host = window.location.host; const prot = ("https:" === document.location.protocol ? "wss://" : "ws://"); const url = prot + host + "/api/v1/ws"; -const clientInfo = { - "token": token, - "hash": window.clientConfig.jsHash, - "version": window.clientConfig.version, -}; const Socket = new Sockette(url, { timeout: 5e3, onopen: e => { console.log("websocket: connected", e); - Socket.send(JSON.stringify(clientInfo)); + Event.publish("websocket.connected", e); }, onmessage: e => { const m = JSON.parse(e.data); diff --git a/frontend/src/component/p-navigation.vue b/frontend/src/component/p-navigation.vue index ca5ca6252..fc1c485e5 100644 --- a/frontend/src/component/p-navigation.vue +++ b/frontend/src/component/p-navigation.vue @@ -10,7 +10,7 @@ - + chevron_right - + photo @@ -54,7 +54,7 @@ - + @@ -89,7 +89,7 @@ - + archive @@ -101,7 +101,7 @@ - + folder @@ -113,7 +113,7 @@ - + @@ -133,7 +133,7 @@ - + favorite @@ -146,7 +146,7 @@ - + label @@ -159,7 +159,7 @@ - + place @@ -172,7 +172,7 @@ - + color_lens @@ -184,7 +184,7 @@ - - - + camera_roll @@ -216,7 +216,7 @@ - + settings diff --git a/internal/api/session.go b/internal/api/session.go index 3bcad7858..b789b3957 100644 --- a/internal/api/session.go +++ b/internal/api/session.go @@ -5,6 +5,7 @@ import ( "github.com/gin-gonic/gin" "github.com/photoprism/photoprism/internal/config" + "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/session" "github.com/photoprism/photoprism/pkg/txt" @@ -33,6 +34,8 @@ func CreateSession(router *gin.RouterGroup, conf *config.Config) { s := gin.H{"token": token, "user": user} + event.Publish("config.updated", event.Data(conf.ClientConfig())) + c.JSON(http.StatusOK, s) }) } diff --git a/internal/api/websocket.go b/internal/api/websocket.go index 528a29754..fd805d2fe 100644 --- a/internal/api/websocket.go +++ b/internal/api/websocket.go @@ -1,13 +1,17 @@ package api import ( + "encoding/json" "net/http" + "sync" "time" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" "github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/event" + "github.com/photoprism/photoprism/internal/session" + "github.com/photoprism/photoprism/pkg/rnd" ) var wsConnection = websocket.Upgrader{ @@ -17,7 +21,19 @@ var wsConnection = websocket.Upgrader{ } var wsTimeout = 60 * time.Second -func wsReader(ws *websocket.Conn) { +type clientInfo struct { + SessionToken string `json:"session"` + JsHash string `json:"js"` + CssHash string `json:"css"` + Version string `json:"version"` +} + +var wsAuth = struct { + authenticated map[string]bool + mutex sync.RWMutex +}{authenticated: make(map[string]bool)} + +func wsReader(ws *websocket.Conn, connId string) { defer ws.Close() ws.SetReadLimit(512) @@ -26,14 +42,30 @@ func wsReader(ws *websocket.Conn) { for { _, m, err := ws.ReadMessage() + if err != nil { break } + log.Debugf("websocket: received %d bytes", len(m)) + + var info clientInfo + + if err := json.Unmarshal(m, &info); err != nil { + log.Error(err) + } else { + log.Debugf("websocket: %+v", info) + + if session.Exists(info.SessionToken) { + wsAuth.mutex.Lock() + wsAuth.authenticated[connId] = true + wsAuth.mutex.Unlock() + } + } } } -func wsWriter(ws *websocket.Conn) { +func wsWriter(ws *websocket.Conn, connId string) { pingTicker := time.NewTicker(10 * time.Second) s := event.Subscribe("log.*", "notify.*", "index.*", "upload.*", "import.*", "config.*", "count.*") @@ -41,6 +73,10 @@ func wsWriter(ws *websocket.Conn) { pingTicker.Stop() event.Unsubscribe(s) ws.Close() + + wsAuth.mutex.Lock() + wsAuth.authenticated[connId] = false + wsAuth.mutex.Unlock() }() for { @@ -51,11 +87,17 @@ func wsWriter(ws *websocket.Conn) { return } case msg := <-s.Receiver: - ws.SetWriteDeadline(time.Now().Add(10 * time.Second)) + wsAuth.mutex.RLock() + auth := wsAuth.authenticated[connId] + wsAuth.mutex.RUnlock() - if err := ws.WriteJSON(gin.H{"event": msg.Name, "data": msg.Fields}); err != nil { - log.Debug(err) - return + if auth { + ws.SetWriteDeadline(time.Now().Add(10 * time.Second)) + + if err := ws.WriteJSON(gin.H{"event": msg.Name, "data": msg.Fields}); err != nil { + log.Debug(err) + return + } } } } @@ -63,6 +105,16 @@ func wsWriter(ws *websocket.Conn) { // GET /api/v1/ws func Websocket(router *gin.RouterGroup, conf *config.Config) { + if router == nil { + log.Error("websocket: router is nil") + return + } + + if conf == nil { + log.Error("websocket: conf is nil") + return + } + router.GET("/ws", func(c *gin.Context) { w := c.Writer r := c.Request @@ -75,10 +127,18 @@ func Websocket(router *gin.RouterGroup, conf *config.Config) { defer ws.Close() + connId := rnd.UUID() + + if conf.Public() { + wsAuth.mutex.Lock() + wsAuth.authenticated[connId] = true + wsAuth.mutex.Unlock() + } + log.Debug("websocket: connected") - go wsWriter(ws) + go wsWriter(ws, connId) - wsReader(ws) + wsReader(ws, connId) }) } diff --git a/internal/config/client.go b/internal/config/client.go index cfe2e17c0..5494addb3 100644 --- a/internal/config/client.go +++ b/internal/config/client.go @@ -12,6 +12,84 @@ import ( // HTTP client / Web UI config values type ClientConfig map[string]interface{} +// PublicClientConfig returns reduced config values for non-public sites. +func (c *Config) PublicClientConfig() ClientConfig { + if c.Public() { + return c.ClientConfig() + } + + jsHash := fs.Hash(c.HttpStaticBuildPath() + "/app.js") + cssHash := fs.Hash(c.HttpStaticBuildPath() + "/app.css") + + // Feature Flags + var flags []string + + if c.Public() { + flags = append(flags, "public") + } + if c.Debug() { + flags = append(flags, "debug") + } + if c.Experimental() { + flags = append(flags, "experimental") + } + if c.ReadOnly() { + flags = append(flags, "readonly") + } + + var noPos = struct { + PhotoUUID string `json:"photo"` + LocationID string `json:"location"` + TakenAt time.Time `json:"utc"` + PhotoLat float64 `json:"lat"` + PhotoLng float64 `json:"lng"` + }{} + + var count = struct { + Photos uint `json:"photos"` + Favorites uint `json:"favorites"` + Private uint `json:"private"` + Stories uint `json:"stories"` + Labels uint `json:"labels"` + Albums uint `json:"albums"` + Countries uint `json:"countries"` + Places uint `json:"places"` + }{} + + result := ClientConfig{ + "flags": strings.Join(flags, " "), + "name": c.Name(), + "url": c.Url(), + "title": c.Title(), + "subtitle": c.Subtitle(), + "description": c.Description(), + "author": c.Author(), + "twitter": c.Twitter(), + "version": c.Version(), + "copyright": c.Copyright(), + "debug": c.Debug(), + "readonly": c.ReadOnly(), + "uploadNSFW": c.UploadNSFW(), + "public": c.Public(), + "experimental": c.Experimental(), + "albums": []string{}, + "cameras": []string{}, + "lenses": []string{}, + "countries": []string{}, + "thumbnails": Thumbnails, + "jsHash": jsHash, + "cssHash": cssHash, + "settings": c.Settings(), + "count": count, + "pos": noPos, + "years": []int{}, + "colors": colors.All.List(), + "categories": []string{}, + } + + return result +} + // ClientConfig returns a loaded and set configuration entity. func (c *Config) ClientConfig() ClientConfig { db := c.Db() diff --git a/internal/server/routes.go b/internal/server/routes.go index fc85f49fa..35fd7a218 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -72,6 +72,7 @@ func registerRoutes(router *gin.Engine, conf *config.Config) { // Default HTML page (client-side routing implemented via Vue.js) router.NoRoute(func(c *gin.Context) { + // Todo: Use PublicClientConfig() c.HTML(http.StatusOK, "index.tmpl", gin.H{"clientConfig": conf.ClientConfig()}) }) }