Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
unixpickle
GitHub Repository: unixpickle/kahoot-hack
Path: blob/master/kahoot/conn.go
10110 views
1
package kahoot
2
3
import (
4
"errors"
5
"net"
6
"net/http"
7
"net/url"
8
"strconv"
9
"sync"
10
"time"
11
12
"github.com/gorilla/websocket"
13
)
14
15
var ErrConnClosed = errors.New("connection closed")
16
var ErrNotSubscribed = errors.New("not subscribed to channel")
17
18
const incomingBufferSize = 16
19
20
type Message map[string]interface{}
21
22
type Conn struct {
23
ws *websocket.Conn
24
25
clientId string
26
gameId string
27
28
channelsLock sync.RWMutex
29
incoming map[string]chan Message
30
outgoing chan Message
31
32
closed chan struct{}
33
}
34
35
// NewConn connects to the kahoot server and performs a handshake
36
// using a given game pin.
37
func NewConn(gameId string) (*Conn, error) {
38
token, err := gameSessionToken(gameId)
39
if err != nil {
40
return nil, errors.New("failed to create session: " + err.Error())
41
}
42
43
conn, err := net.Dial("tcp", "kahoot.it:443")
44
if err != nil {
45
return nil, err
46
}
47
48
url, err := url.Parse("wss://kahoot.it/cometd/" + gameId + "/" + token)
49
if err != nil {
50
return nil, err
51
}
52
reqHeader := http.Header{}
53
reqHeader.Set("Origin", "https://kahoot.it")
54
reqHeader.Set("Cookie", "no.mobitroll.session="+gameId)
55
ws, _, err := websocket.NewClient(conn, url, reqHeader, 100, 100)
56
if err != nil {
57
return nil, err
58
}
59
60
c := &Conn{
61
ws: ws,
62
gameId: gameId,
63
incoming: map[string]chan Message{
64
"/meta/connect": make(chan Message, incomingBufferSize),
65
"/meta/disconnect": make(chan Message, incomingBufferSize),
66
"/meta/handshake": make(chan Message, incomingBufferSize),
67
"/meta/subscribe": make(chan Message, incomingBufferSize),
68
},
69
outgoing: make(chan Message),
70
closed: make(chan struct{}),
71
}
72
73
go c.readLoop()
74
go c.writeLoop()
75
76
err = c.Send("/meta/handshake", Message{
77
"version": "1.0",
78
"minimumVersion": "1.0",
79
"supportedConnectionTypes": []string{"websocket", "long-polling"},
80
"advice": map[string]int{"timeout": 60000, "interval": 0},
81
})
82
if err != nil {
83
c.Close()
84
return nil, err
85
}
86
87
response, err := c.Receive("/meta/handshake")
88
if err != nil {
89
c.Close()
90
return nil, err
91
}
92
93
if clientId, ok := response["clientId"].(string); !ok {
94
c.Close()
95
return nil, errors.New("invalid handshake response")
96
} else {
97
c.clientId = clientId
98
}
99
100
for _, service := range []string{"controller", "player", "status"} {
101
if err := c.Subscribe("/service/" + service); err != nil {
102
c.Close()
103
return nil, err
104
}
105
}
106
107
err = c.Send("/meta/connect", Message{
108
"connectionType": "websocket",
109
"advice": map[string]int{"timeout": 0},
110
})
111
if err != nil {
112
c.Close()
113
return nil, err
114
}
115
connResp, err := c.Receive("/meta/connect")
116
if err != nil {
117
c.Close()
118
return nil, err
119
}
120
if success, ok := connResp["successful"].(bool); !ok || !success {
121
c.Close()
122
return nil, errors.New("did not receive successful response")
123
}
124
125
go c.keepAliveLoop()
126
127
return c, nil
128
}
129
130
// Login tells the server our nickname.
131
func (c *Conn) Login(nickname string) error {
132
m := Message{
133
"data": Message{
134
"type": "login",
135
"gameid": c.gameId,
136
"host": "kahoot.it",
137
"name": nickname,
138
},
139
}
140
if err := c.Send("/service/controller", m); err != nil {
141
return err
142
}
143
144
for {
145
resp, err := c.Receive("/service/controller")
146
if err != nil {
147
return err
148
} else if data, ok := resp["data"].(map[string]interface{}); !ok {
149
continue
150
} else if typeStr, ok := data["type"].(string); !ok || typeStr != "loginResponse" {
151
continue
152
} else {
153
return nil
154
}
155
}
156
}
157
158
// Close terminates the connection, waiting synchronously for the
159
// incoming channels to close.
160
func (c *Conn) Close() {
161
c.ws.Close()
162
<-c.closed
163
}
164
165
// GracefulClose closes the connection gracefully, telling the other end that
166
// we are disconnecting.
167
func (c *Conn) GracefulClose() {
168
defer c.Close()
169
if c.Send("/meta/disconnect", Message{}) != nil {
170
return
171
}
172
c.Receive("/meta/disconnect")
173
}
174
175
// Send transmits a message to the server over a channel.
176
func (c *Conn) Send(channel string, m Message) error {
177
packet := Message{}
178
for k, v := range m {
179
packet[k] = v
180
}
181
packet["channel"] = channel
182
183
select {
184
case c.outgoing <- packet:
185
case <-c.closed:
186
return ErrConnClosed
187
}
188
return nil
189
}
190
191
// Subscribe tells the server that we wish to receive messages
192
// on a given channel.
193
func (c *Conn) Subscribe(name string) error {
194
c.channelsLock.Lock()
195
if c.incoming == nil {
196
c.channelsLock.Unlock()
197
return ErrConnClosed
198
} else if _, ok := c.incoming[name]; ok {
199
c.channelsLock.Unlock()
200
return nil
201
}
202
c.incoming[name] = make(chan Message, incomingBufferSize)
203
c.channelsLock.Unlock()
204
c.Send("/meta/subscribe", Message{"subscription": name})
205
nextMsg, err := c.Receive("/meta/subscribe")
206
if err != nil {
207
return err
208
} else if success, ok := nextMsg["successful"].(bool); !ok || !success {
209
return errors.New("did not receive successful response")
210
}
211
return nil
212
}
213
214
// Receive returns the next message on a given channel.
215
// You must Subscribe() to the channel before Receiving on it.
216
func (c *Conn) Receive(channel string) (Message, error) {
217
c.channelsLock.RLock()
218
if c.incoming == nil {
219
c.channelsLock.RUnlock()
220
return nil, ErrConnClosed
221
}
222
ch, ok := c.incoming[channel]
223
c.channelsLock.RUnlock()
224
if !ok {
225
return nil, ErrNotSubscribed
226
}
227
if res := <-ch; res != nil {
228
return res, nil
229
} else {
230
return nil, ErrConnClosed
231
}
232
}
233
234
func (c *Conn) readLoop() {
235
defer func() {
236
c.channelsLock.Lock()
237
defer c.channelsLock.Unlock()
238
for _, ch := range c.incoming {
239
close(ch)
240
}
241
c.incoming = nil
242
close(c.closed)
243
}()
244
for {
245
var msgs []Message
246
err := c.ws.ReadJSON(&msgs)
247
if err != nil {
248
return
249
}
250
for _, msg := range msgs {
251
if chName, ok := msg["channel"].(string); !ok {
252
return
253
} else {
254
c.channelsLock.RLock()
255
ch, ok := c.incoming[chName]
256
c.channelsLock.RUnlock()
257
if ok {
258
// NOTE: the select allows us to drop packets from channels
259
// that nobody cares about (e.g. /meta/connect).
260
select {
261
case ch <- msg:
262
default:
263
}
264
}
265
}
266
}
267
}
268
}
269
270
func (c *Conn) writeLoop() {
271
id := 0
272
for {
273
select {
274
case msg := <-c.outgoing:
275
id++
276
msg["id"] = strconv.Itoa(id)
277
if msg["channel"] != "/meta/handshake" {
278
msg["clientId"] = c.clientId
279
}
280
if c.ws.WriteJSON([]Message{msg}) != nil {
281
c.ws.Close()
282
return
283
}
284
case <-c.closed:
285
return
286
}
287
}
288
}
289
290
func (c *Conn) keepAliveLoop() {
291
for {
292
delay := time.After(time.Second * 5)
293
select {
294
case <-delay:
295
case <-c.closed:
296
return
297
}
298
c.Send("/meta/connect", Message{"connectionType": "websocket"})
299
}
300
}
301
302