-
Notifications
You must be signed in to change notification settings - Fork 116
/
handlers.go
339 lines (291 loc) · 8.54 KB
/
handlers.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
package main
import (
"context"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"github.com/go-chi/chi"
"github.com/gorilla/websocket"
"github.com/knadh/niltalk/internal/hub"
"golang.org/x/crypto/bcrypt"
)
const (
hasAuth = 1 << iota
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.
type jsonResp struct {
Error *string `json:"error"`
Data interface{} `json:"data"`
}
// tplWrap is the envelope for all HTML template executions.
type tpl struct {
Config *hub.Config
Data tplData
}
type tplData struct {
Title string
Description string
Room interface{}
Auth bool
}
type reqRoom struct {
Name string `json:"name"`
Handle string `json:"handle"`
Password string `json:"password"`
}
var upgrader = websocket.Upgrader{CheckOrigin: func(r *http.Request) bool {
return true
}}
// handleIndex renders the homepage.
func handleIndex(w http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context().Value("ctx").(*reqCtx)
app = ctx.app
)
respondHTML("index", tplData{
Title: app.cfg.Name,
}, http.StatusOK, w, app)
}
// handleRoomPage renders the chat room page.
func handleRoomPage(w http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context().Value("ctx").(*reqCtx)
app = ctx.app
room = ctx.room
)
if room == nil {
respondHTML("room-not-found", tplData{}, http.StatusNotFound, w, app)
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", out, http.StatusOK, w, app)
}
// handleLogin authenticates a peer into a room.
func handleLogin(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
}
var req reqRoom
if err := readJSONReq(r, &req); err != nil {
respondJSON(w, nil, errors.New("error parsing JSON request"), http.StatusBadRequest)
return
}
if req.Handle == "" {
h, err := hub.GenerateGUID(8)
if err != nil {
app.logger.Printf("error generating uniq handle: %v", err)
respondJSON(w, nil, errors.New("error generating uniq handle"), http.StatusInternalServerError)
return
}
req.Handle = h
}
// Validate password.
if err := bcrypt.CompareHashAndPassword(room.Password, []byte(req.Password)); err != nil {
respondJSON(w, nil, errors.New("incorrect password"), http.StatusForbidden)
return
}
// Register a new session for the peer in the DB.
sessID, err := hub.GenerateGUID(32)
if err != nil {
app.logger.Printf("error generating session ID: %v", err)
respondJSON(w, nil, errors.New("error generating session ID"), http.StatusInternalServerError)
return
}
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
}
// Set the session cookie.
ck := &http.Cookie{Name: app.cfg.SessionCookie, Value: sessID, Path: "/"}
http.SetCookie(w, ck)
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 (
ctx = r.Context().Value("ctx").(*reqCtx)
app = ctx.app
room = ctx.room
)
if ctx.sess.ID == "" {
respondJSON(w, nil, errors.New("invalid session"), http.StatusForbidden)
return
}
// 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)
return
}
// Create a new peer instance and add to the room.
room.AddPeer(ctx.sess.ID, ctx.sess.Handle, ws)
}
// respondJSON responds to an HTTP request with a generic payload or an error.
func respondJSON(w http.ResponseWriter, data interface{}, err error, statusCode int) {
if statusCode == 0 {
statusCode = http.StatusOK
}
w.WriteHeader(statusCode)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
out := jsonResp{Data: data}
if err != nil {
e := err.Error()
out.Error = &e
}
b, err := json.Marshal(out)
if err != nil {
logger.Printf("error marshalling JSON response: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Write(b)
}
// respondHTML responds to an HTTP request with the HTML output of a given template.
func respondHTML(tplName string, data tplData, statusCode int, w http.ResponseWriter, app *App) {
if statusCode > 0 {
w.WriteHeader(statusCode)
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
err := app.tpl.ExecuteTemplate(w, tplName, tpl{
Config: app.cfg,
Data: data,
})
if err != nil {
app.logger.Printf("error rendering template %s: %s", tplName, err)
w.Write([]byte("error rendering template"))
}
}
// handleCreateRoom handles the creation of a new room.
func handleCreateRoom(w http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context().Value("ctx").(*reqCtx)
app = ctx.app
)
var req reqRoom
if err := readJSONReq(r, &req); err != nil {
respondJSON(w, nil, errors.New("error parsing JSON request"), http.StatusBadRequest)
return
}
if req.Name != "" && (len(req.Name) < 3 || len(req.Name) > 100) {
respondJSON(w, nil, errors.New("invalid room name (6 - 100 chars)"), http.StatusBadRequest)
return
}
if len(req.Password) < 6 || len(req.Password) > 100 {
respondJSON(w, nil, errors.New("invalid password (6 - 100 chars)"), http.StatusBadRequest)
return
}
// Hash the password.
pwdHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 8)
if err != nil {
app.logger.Printf("error hashing password: %v", err)
respondJSON(w, "Error hashing password", nil, http.StatusInternalServerError)
return
}
// Create and activate the new room.
room, err := app.hub.AddRoom(req.Name, pwdHash)
if err != nil {
respondJSON(w, nil, err, http.StatusInternalServerError)
return
}
respondJSON(w, struct {
ID string `json:"id"`
}{room.ID}, nil, http.StatusOK)
}
// wrap is a middleware that handles auth and room check for various HTTP handlers.
// It attaches the app and room contexts to handlers.
func wrap(next http.HandlerFunc, app *App, opts uint8) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var (
req = &reqCtx{app: app}
roomID = chi.URLParam(r, "roomID")
)
// Check if the request is authenticated.
if opts&hasAuth != 0 {
ck, _ := r.Cookie(app.cfg.SessionCookie)
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,
}
}
}
// Check if the room is valid and active.
if opts&hasRoom != 0 {
// If the room's not found, req.room will be null in the target
// handler. It's the handler's responsibility to throw an error,
// API or HTML response.
room, err := app.hub.ActivateRoom(roomID)
if err == nil {
req.room = room
}
}
// Attach the request context.
ctx := context.WithValue(r.Context(), "ctx", req)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// readJSONReq reads the JSON body from a request and unmarshals it to the given target.
func readJSONReq(r *http.Request, o interface{}) error {
defer r.Body.Close()
b, err := ioutil.ReadAll(r.Body)
if err != nil {
return err
}
return json.Unmarshal(b, o)
}