Skip to content

Commit

Permalink
Refactor session to actuall remember sessions
Browse files Browse the repository at this point in the history
  • Loading branch information
knadh committed Apr 4, 2020
1 parent da057ad commit 89dfeed
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 54 deletions.
91 changes: 59 additions & 32 deletions handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"

Expand All @@ -19,10 +18,16 @@ const (
hasRoom
)

type sess struct {
ID string
Handle string
}

// reqCtx is the context injected into every request.
type reqCtx struct {
app *App
room *hub.Room
sess sess
}

// jsonResp is the envelope for all JSON API responses.
Expand All @@ -41,6 +46,7 @@ type tplData struct {
Title string
Description string
Room interface{}
Auth bool
}

type reqRoom struct {
Expand Down Expand Up @@ -77,14 +83,19 @@ func handleRoomPage(w http.ResponseWriter, r *http.Request) {
return
}

out := tplData{
Title: room.Name,
Room: room,
}
if ctx.sess.ID != "" {
out.Auth = true
}

// Disable browser caching.
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
respondHTML("room", tplData{
Title: room.Name,
Room: room,
}, http.StatusNotFound, w, app)
respondHTML("room", out, http.StatusNotFound, w, app)
}

// handleLogin authenticates a peer into a room.
Expand Down Expand Up @@ -120,7 +131,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
return
}

if err := app.hub.Store.AddSession(sessID, room.ID, app.cfg.RoomAge); err != nil {
if err := app.hub.Store.AddSession(sessID, req.Handle, room.ID, app.cfg.RoomAge); err != nil {
app.logger.Printf("error creating session: %v", err)
respondJSON(w, nil, errors.New("error creating session"), http.StatusInternalServerError)
return
Expand All @@ -132,6 +143,31 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
respondJSON(w, true, nil, http.StatusOK)
}

// handleLogout logs out a peer.
func handleLogout(w http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context().Value("ctx").(*reqCtx)
app = ctx.app
room = ctx.room
)

if room == nil {
respondJSON(w, nil, errors.New("room is invalid or has expired"), http.StatusBadRequest)
return
}

if err := app.hub.Store.RemoveSession(ctx.sess.ID, room.ID); err != nil {
app.logger.Printf("error removing session: %v", err)
respondJSON(w, nil, errors.New("error removing session"), http.StatusInternalServerError)
return
}

// Delete the session cookie.
ck := &http.Cookie{Name: app.cfg.SessionCookie, Value: "", MaxAge: -1, Path: "/"}
http.SetCookie(w, ck)
respondJSON(w, true, nil, http.StatusOK)
}

// handleWS handles incoming connections.
func handleWS(w http.ResponseWriter, r *http.Request) {
var (
Expand All @@ -140,29 +176,20 @@ func handleWS(w http.ResponseWriter, r *http.Request) {
room = ctx.room
)

// Create the WS connection.
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
app.logger.Printf("Websocket upgrade failed: %s: %v", r.RemoteAddr, err)
if ctx.sess.ID == "" {
respondJSON(w, nil, errors.New("invalid session"), http.StatusBadRequest)
return
}

// Generate an ID for the peer.
peerID, err := hub.GenerateGUID(32)
// Create the WS connection.
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
app.logger.Printf("error generating peer ID: %v", err)
http.Error(w, "error generating peer ID", http.StatusInternalServerError)
app.logger.Printf("Websocket upgrade failed: %s: %v", r.RemoteAddr, err)
return
}

// Assign a handle to the peer.
handle := r.URL.Query().Get("handle")
if len(handle) < 3 {
handle = fmt.Sprintf(app.cfg.PeerHandleFormat, peerID[:4])
}

// Create a new peer instance and add to the room.
room.AddPeer(peerID, handle, ws)
room.AddPeer(ctx.sess.ID, ctx.sess.Handle, ws)
}

// respondJSON responds to an HTTP request with a generic payload or an error.
Expand Down Expand Up @@ -260,17 +287,17 @@ func wrap(next http.HandlerFunc, app *App, opts uint8) http.HandlerFunc {
// Check if the request is authenticated.
if opts&hasAuth != 0 {
ck, _ := r.Cookie(app.cfg.SessionCookie)
if ck == nil || ck.Value == "" {
respondJSON(w, nil, errors.New("session is invalid or has expired"), http.StatusForbidden)
return
}
if ok, err := app.hub.Store.SessionExists(ck.Value, roomID); err != nil {
app.logger.Printf("error checking session: %v", err)
respondJSON(w, nil, errors.New("error checking session"), http.StatusForbidden)
return
} else if !ok {
respondJSON(w, nil, errors.New("session is invalid or has expired"), http.StatusForbidden)
return
if ck != nil && ck.Value != "" {
s, err := app.hub.Store.GetSession(ck.Value, roomID)
if err != nil {
app.logger.Printf("error checking session: %v", err)
respondJSON(w, nil, errors.New("error checking session"), http.StatusForbidden)
return
}
req.sess = sess{
ID: s.ID,
Handle: s.Handle,
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,11 @@ func main() {

// API.
r.Post("/api/rooms/{roomID}/login", wrap(handleLogin, app, hasRoom))
r.Delete("/api/rooms/{roomID}/login", wrap(handleLogout, app, hasAuth|hasRoom))
r.Post("/api/rooms", wrap(handleCreateRoom, app, 0))

// Views.
r.Get("/r/{roomID}", wrap(handleRoomPage, app, hasRoom))
r.Get("/r/{roomID}", wrap(handleRoomPage, app, hasAuth|hasRoom))
r.Get("/theme/*", func(w http.ResponseWriter, r *http.Request) {
app.fs.FileServer().ServeHTTP(w, r)
})
Expand Down
20 changes: 12 additions & 8 deletions store/redis/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,34 +142,38 @@ func (r *Redis) RemoveRoom(id string) error {
}

// AddSession adds a sessionID room to the store.
func (r *Redis) AddSession(sessID, roomID string, ttl time.Duration) error {
func (r *Redis) AddSession(sessID, handle, roomID string, ttl time.Duration) error {
c := r.pool.Get()
defer c.Close()

key := fmt.Sprintf(r.cfg.PrefixSession, roomID)
c.Send("SADD", key, sessID)
c.Send("HMSET", key, sessID, handle)
c.Send("EXPIRE", key, ttl.Seconds)
return c.Flush()
}

// SessionExists adds a sessionID room to the store.
func (r *Redis) SessionExists(sessID, roomID string) (bool, error) {
// GetSession retrieves a peer session from th store.
func (r *Redis) GetSession(sessID, roomID string) (store.Sess, error) {
c := r.pool.Get()
defer c.Close()

ok, err := redis.Bool(c.Do("SISMEMBER", fmt.Sprintf(r.cfg.PrefixSession, roomID), sessID))
h, err := redis.String(c.Do("HGET", fmt.Sprintf(r.cfg.PrefixSession, roomID), sessID))
if err != nil && err != redis.ErrNil {
return false, err
return store.Sess{}, err
}
return ok, err

return store.Sess{
ID: sessID,
Handle: h,
}, nil
}

// RemoveSession deletes a session ID from a room.
func (r *Redis) RemoveSession(sessID, roomID string) error {
c := r.pool.Get()
defer c.Close()

_, err := redis.Bool(c.Do("SDEL", fmt.Sprintf(r.cfg.PrefixSession, roomID), sessID))
_, err := redis.Bool(c.Do("HDEL", fmt.Sprintf(r.cfg.PrefixSession, roomID), sessID))
return err
}

Expand Down
10 changes: 8 additions & 2 deletions store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ type Store interface {
RoomExists(id string) (bool, error)
RemoveRoom(id string) error

AddSession(sessID, roomID string, ttl time.Duration) error
SessionExists(sessID, roomID string) (bool, error)
AddSession(sessID, handle, roomID string, ttl time.Duration) error
GetSession(sessID, roomID string) (Sess, error)
RemoveSession(roomID, sessID string) error
ClearSessions(roomID string) error
}
Expand All @@ -27,5 +27,11 @@ type Room struct {
CreatedAt time.Time `json:"created_at"`
}

// Sess represents an authenticated peer session.
type Sess struct {
ID string `json:"id"`
Handle string `json:"name"`
}

// ErrRoomNotFound indicates that the requested room was not found.
var ErrRoomNotFound = errors.New("room not found")
9 changes: 4 additions & 5 deletions theme/static/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@ var Client = new function () {


// Initialize and connect the websocket.
this.init = function (roomID, handle) {
this.init = function (roomID) {
wsURL = document.location.protocol.replace(/http(s?):/, "ws$1:") +
document.location.host +
"/ws/" + _room.id + "?handle=" + handle;
document.location.host + "/ws/" + roomID;
};

// Peer identification info.
Expand Down Expand Up @@ -84,7 +83,7 @@ var Client = new function () {
this.getPeers = function () {
send({ "type": MsgType.PeerList });
};

// send a message
this.sendMessage = function (typ, data) {
send({ "type": typ, "data": data });
Expand All @@ -108,7 +107,7 @@ var Client = new function () {

// trigger event callbacks
function trigger(typ, data) {
if(!triggers.hasOwnProperty(typ)) {
if (!triggers.hasOwnProperty(typ)) {
return;
}

Expand Down
10 changes: 9 additions & 1 deletion theme/static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -392,9 +392,13 @@ form .help {
font-size: 1em;
padding: 5px 30px;
}
.form-chat .controls .btn-dispose {
.form-chat .controls .right {
float: right;
}
.form-chat .controls .right a {
display: inline-block;
margin-left: 15px;
}

@media screen and (max-width: 990px) {
body {
Expand All @@ -403,6 +407,10 @@ form .help {

.chat .messages {
width: 100%;
height: 58vh;
}
.form-chat textarea {
height: 150px;
}
.chat .sidebar {
width: 25%;
Expand Down
28 changes: 26 additions & 2 deletions theme/static/vue.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ var app = new Vue({
created: function () {
this.initClient();
this.initTimers();

if (window.hasOwnProperty("_room") && _room.auth) {
this.toggleChat();
Client.init(_room.id);
Client.connect();
}
},
computed: {
Client() {
Expand Down Expand Up @@ -117,7 +123,7 @@ var app = new Vue({
this.clear();
this.deNotify();
this.toggleChat();
Client.init(_room.id, handle);
Client.init(_room.id);
Client.connect();
})
.catch(err => {
Expand Down Expand Up @@ -160,6 +166,24 @@ var app = new Vue({
this.typingTimer = null;
},

handleLogout() {
if (!confirm("Logout?")) {
return;
}
fetch("/api/rooms/" + _room.id + "/login", {
method: "delete",
headers: { "Content-Type": "application/json; charset=utf-8" }
})
.then(resp => resp.json())
.then(resp => {
this.toggleChat();
document.location.reload();
})
.catch(err => {
this.notify(err, notifType.error);
});
},

handleDisposeRoom() {
if (!confirm("Disconnect all peers and destroy this room?")) {
return;
Expand Down Expand Up @@ -233,7 +257,7 @@ var app = new Vue({
this.chatOn = !this.chatOn;

this.$nextTick().then(function () {
if (!this.chatOn) {
if (!this.chatOn && this.$refs["form-password"]) {
this.$refs["form-password"].focus();
return
}
Expand Down
5 changes: 3 additions & 2 deletions theme/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
<link href="/theme/static/style.css" rel="stylesheet" />
<script>
{{ if .Data.Room }}
const _room = {
window._room = {
id: "{{ .Data.Room.ID }}",
name: "{{ .Data.Room.Name }}"
name: "{{ .Data.Room.Name }}",
auth: {{ .Data.Auth }}
};
{{ end }}
</script>
Expand Down
6 changes: 5 additions & 1 deletion theme/templates/room.html
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,11 @@ <h2 class="title">
placeholder="Message" class="charlimited" maxlength="{{ .Config.MaxMessageLen }}"></textarea>
<div class="controls">
<button type="submit" class="button">Send</button>
<button v-on:click.prevent="handleDisposeRoom" class="btn-dispose">Dispose &times;</button>

<div class="right">
<a href="" v-on:click.prevent="handleLogout" class="btn-dispose">Logout</a>
<a href="" v-on:click.prevent="handleDisposeRoom" class="btn-dispose">Dispose &times;</a>
</div>
<!-- <div class="sounds">
<input v-model="hasSound" type="checkbox" checked="true" id="chk-sounds" />
<label for="chk-sounds"></label>
Expand Down

0 comments on commit 89dfeed

Please sign in to comment.