-
Notifications
You must be signed in to change notification settings - Fork 18
/
websocket.cr
144 lines (119 loc) · 4.43 KB
/
websocket.cr
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
module Grip
module Controllers
class WebSocket
alias Context = HTTP::Server::Context
alias Socket = HTTP::WebSocket::Protocol
include HTTP::Handler
include Helpers::Singleton
getter? closed = false
# :nodoc:
def initialize
@buffer = Bytes.new(4096)
@current_message = IO::Memory.new
end
def on_open(context : Context, socket : Socket) : Void
end
def on_ping(context : Context, socket : Socket, message : String) : Void
end
def on_pong(context : Context, socket : Socket, message : String) : Void
end
def on_message(context : Context, socket : Socket, message : String) : Void
end
def on_binary(context : Context, socket : Socket, binary : Bytes) : Void
end
def on_close(context : Context, socket : Socket, close_code : HTTP::WebSocket::CloseCode | Int?, message : String) : Void
end
protected def check_open
raise IO::Error.new "Closed socket" if closed?
end
def run(context, socket)
on_open(context, socket)
loop do
begin
info = socket.receive(@buffer)
rescue
on_close(context, socket, HTTP::WebSocket::CloseCode::AbnormalClosure, "")
@closed = true
break
end
case info.opcode
when .ping?
@current_message.write @buffer[0, info.size]
if info.final
on_ping(context, socket, @current_message.to_s)
@current_message.clear
end
when .pong?
@current_message.write @buffer[0, info.size]
if info.final
on_pong(context, socket, @current_message.to_s)
@current_message.clear
end
when .text?
@current_message.write @buffer[0, info.size]
if info.final
on_message(context, socket, @current_message.to_s)
@current_message.clear
end
when .binary?
@current_message.write @buffer[0, info.size]
if info.final
on_binary(context, socket, @current_message.to_slice)
@current_message.clear
end
when .close?
@current_message.write @buffer[0, info.size]
if info.final
@current_message.rewind
if @current_message.size >= 2
code = @current_message.read_bytes(UInt16, IO::ByteFormat::NetworkEndian).to_i
code = HTTP::WebSocket::CloseCode.new(code)
else
code = HTTP::WebSocket::CloseCode::NoStatusReceived
end
message = @current_message.gets_to_end
on_close(context, socket, code, message)
socket.close(code, message)
@current_message.clear
break
end
when HTTP::WebSocket::Protocol::Opcode::CONTINUATION
# TODO: (asterite) I think this is good, but this case wasn't originally handled
end
end
end
def call(context : Context) : Context
unless websocket_upgrade_request? context.request
raise Exceptions::BadRequest.new
end
response = context.response
version = context.request.headers["Sec-WebSocket-Version"]?
unless version == HTTP::WebSocket::Protocol::VERSION
response.status = :upgrade_required
response.headers["Sec-WebSocket-Version"] = HTTP::WebSocket::Protocol::VERSION
return context
end
key = context.request.headers["Sec-WebSocket-Key"]?
unless key
response.respond_with_status(:bad_request)
return context
end
accept_code = HTTP::WebSocket::Protocol.key_challenge(key)
response.status = :switching_protocols
response.headers["Upgrade"] = "websocket"
response.headers["Connection"] = "Upgrade"
response.headers["Sec-WebSocket-Accept"] = accept_code
response.upgrade do |io|
socket = HTTP::WebSocket::Protocol.new(io, sync_close: true)
run(context, socket)
end
context
end
private def websocket_upgrade_request?(request)
return false unless upgrade = request.headers["Upgrade"]?
return false unless upgrade.compare("websocket", case_insensitive: true) == 0
request.headers.includes_word?("Connection", "Upgrade")
end
end
end
end