Add Websocket authentication #216
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
f569c3adb6
commit
7342d5194a
|
@ -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");
|
||||
|
|
|
@ -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 = "/";
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
</v-toolbar>
|
||||
<v-navigation-drawer
|
||||
v-model="drawer"
|
||||
:mini-variant="mini"
|
||||
:mini-variant="mini || !auth"
|
||||
class="p-navigation-sidebar navigation"
|
||||
width="270"
|
||||
fixed dark app
|
||||
|
@ -35,14 +35,14 @@
|
|||
</v-list>
|
||||
</v-toolbar>
|
||||
|
||||
<v-list class="pt-3">
|
||||
<v-list class="pt-3" v-if="auth">
|
||||
<v-list-tile v-if="mini" @click.stop="mini = !mini" class="p-navigation-expand">
|
||||
<v-list-tile-action>
|
||||
<v-icon>chevron_right</v-icon>
|
||||
</v-list-tile-action>
|
||||
</v-list-tile>
|
||||
|
||||
<v-list-tile v-if="mini && auth" to="/photos" @click="" class="p-navigation-photos">
|
||||
<v-list-tile v-if="mini" to="/photos" @click="" class="p-navigation-photos">
|
||||
<v-list-tile-action>
|
||||
<v-icon>photo</v-icon>
|
||||
</v-list-tile-action>
|
||||
|
@ -54,7 +54,7 @@
|
|||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
||||
<v-list-group v-if="!mini && auth" prepend-icon="photo" no-action>
|
||||
<v-list-group v-if="!mini" prepend-icon="photo" no-action>
|
||||
<v-list-tile slot="activator" to="/photos" @click.stop="" class="p-navigation-photos">
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title>
|
||||
|
@ -89,7 +89,7 @@
|
|||
</v-list-tile>
|
||||
</v-list-group>
|
||||
|
||||
<v-list-tile v-if="mini && auth" to="/archive" @click="" class="p-navigation-archive">
|
||||
<v-list-tile v-if="mini" to="/archive" @click="" class="p-navigation-archive">
|
||||
<v-list-tile-action>
|
||||
<v-icon>archive</v-icon>
|
||||
</v-list-tile-action>
|
||||
|
@ -101,7 +101,7 @@
|
|||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
||||
<v-list-tile v-if="mini && auth" to="/albums" @click="">
|
||||
<v-list-tile v-if="mini" to="/albums" @click="">
|
||||
<v-list-tile-action>
|
||||
<v-icon>folder</v-icon>
|
||||
</v-list-tile-action>
|
||||
|
@ -113,7 +113,7 @@
|
|||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
||||
<v-list-group v-if="!mini && auth" prepend-icon="folder" no-action>
|
||||
<v-list-group v-if="!mini" prepend-icon="folder" no-action>
|
||||
<v-list-tile slot="activator" to="/albums" @click.stop="">
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title>
|
||||
|
@ -133,7 +133,7 @@
|
|||
</v-list-tile>
|
||||
</v-list-group>
|
||||
|
||||
<v-list-tile to="/favorites" @click="" class="p-navigation-favorites" v-if="auth">
|
||||
<v-list-tile to="/favorites" @click="" class="p-navigation-favorites">
|
||||
<v-list-tile-action>
|
||||
<v-icon>favorite</v-icon>
|
||||
</v-list-tile-action>
|
||||
|
@ -146,7 +146,7 @@
|
|||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
||||
<v-list-tile to="/labels" @click="" class="p-navigation-labels" v-if="auth">
|
||||
<v-list-tile to="/labels" @click="" class="p-navigation-labels">
|
||||
<v-list-tile-action>
|
||||
<v-icon>label</v-icon>
|
||||
</v-list-tile-action>
|
||||
|
@ -159,7 +159,7 @@
|
|||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
||||
<v-list-tile :to="{ name: 'places' }" @click="" class="p-navigation-places" v-if="auth">
|
||||
<v-list-tile :to="{ name: 'places' }" @click="" class="p-navigation-places">
|
||||
<v-list-tile-action>
|
||||
<v-icon>place</v-icon>
|
||||
</v-list-tile-action>
|
||||
|
@ -172,7 +172,7 @@
|
|||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
||||
<v-list-tile to="/discover" @click="" class="p-navigation-discover" v-if="config.experimental && auth">
|
||||
<v-list-tile to="/discover" @click="" class="p-navigation-discover" v-if="config.experimental">
|
||||
<v-list-tile-action>
|
||||
<v-icon>color_lens</v-icon>
|
||||
</v-list-tile-action>
|
||||
|
@ -184,7 +184,7 @@
|
|||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
||||
<!-- v-list-tile to="/events" @click="" class="p-navigation-events" v-if="auth">
|
||||
<!-- v-list-tile to="/events" @click="" class="p-navigation-events">
|
||||
<v-list-tile-action>
|
||||
<v-icon>date_range</v-icon>
|
||||
</v-list-tile-action>
|
||||
|
@ -194,7 +194,7 @@
|
|||
</v-list-tile-content>
|
||||
</v-list-tile -->
|
||||
|
||||
<!-- v-list-tile to="/people" @click="" class="p-navigation-people" v-if="auth">
|
||||
<!-- v-list-tile to="/people" @click="" class="p-navigation-people">
|
||||
<v-list-tile-action>
|
||||
<v-icon>people</v-icon>
|
||||
</v-list-tile-action>
|
||||
|
@ -204,7 +204,7 @@
|
|||
</v-list-tile-content>
|
||||
</v-list-tile -->
|
||||
|
||||
<v-list-tile to="/library" @click="" class="p-navigation-library" v-if="auth">
|
||||
<v-list-tile to="/library" @click="" class="p-navigation-library">
|
||||
<v-list-tile-action>
|
||||
<v-icon>camera_roll</v-icon>
|
||||
</v-list-tile-action>
|
||||
|
@ -216,7 +216,7 @@
|
|||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
||||
<v-list-tile to="/settings" @click="" class="p-navigation-settings" v-if="auth">
|
||||
<v-list-tile to="/settings" @click="" class="p-navigation-settings">
|
||||
<v-list-tile-action>
|
||||
<v-icon>settings</v-icon>
|
||||
</v-list-tile-action>
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()})
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue