ruflet_server 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +3 -0
- data/lib/ruflet/server/web_socket_connection.rb +128 -0
- data/lib/ruflet/server/wire_codec.rb +225 -0
- data/lib/ruflet/server.rb +407 -0
- data/lib/ruflet/version.rb +5 -0
- data/lib/ruflet_server.rb +17 -0
- metadata +59 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9ce7a082041b04a7208cf20bb1d541f6a4902361c5a31b1c188f509ebe321369
|
|
4
|
+
data.tar.gz: 4af18bd3e91a82bc38ce58f626005e20fb9e480bafe8ecefa768c81b77c9bf05
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9cdf0804b857072621d1a83156b13e7b1a9b799c3152182b50c91c1fb733e83e44965d6e197729ee1589519566c913ab2e4f0475c6faffc08e79749cbac119e8
|
|
7
|
+
data.tar.gz: b30664476e50b799ac372795314493f3f35c5f76643e0a8967e4ded5e7e934f49d1c808706bf7593fb15f71e2f71693fb3bad870c733470591dda3ba1e7012c5
|
data/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruflet
|
|
4
|
+
class WebSocketConnection
|
|
5
|
+
def initialize(socket)
|
|
6
|
+
@socket = socket
|
|
7
|
+
@write_mutex = Mutex.new
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def session_key
|
|
11
|
+
@socket.object_id
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def closed?
|
|
15
|
+
@socket.closed?
|
|
16
|
+
rescue IOError
|
|
17
|
+
true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def send_binary(payload)
|
|
21
|
+
send_frame(0x2, payload.to_s.b)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def send_text(payload)
|
|
25
|
+
send_frame(0x1, payload.to_s.b)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def read_message
|
|
29
|
+
frame = read_frame
|
|
30
|
+
return nil if frame.nil?
|
|
31
|
+
|
|
32
|
+
opcode = frame[:opcode]
|
|
33
|
+
payload = frame[:payload]
|
|
34
|
+
|
|
35
|
+
case opcode
|
|
36
|
+
when 0x8
|
|
37
|
+
close
|
|
38
|
+
nil
|
|
39
|
+
when 0x9
|
|
40
|
+
send_frame(0xA, payload)
|
|
41
|
+
read_message
|
|
42
|
+
when 0xA
|
|
43
|
+
read_message
|
|
44
|
+
when 0x1, 0x2
|
|
45
|
+
payload
|
|
46
|
+
else
|
|
47
|
+
read_message
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def close
|
|
52
|
+
return if closed?
|
|
53
|
+
|
|
54
|
+
begin
|
|
55
|
+
@socket.close
|
|
56
|
+
rescue IOError
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def read_frame
|
|
64
|
+
header = read_exact(2)
|
|
65
|
+
return nil if header.nil?
|
|
66
|
+
|
|
67
|
+
b1 = header.getbyte(0)
|
|
68
|
+
b2 = header.getbyte(1)
|
|
69
|
+
|
|
70
|
+
masked = (b2 & 0x80) != 0
|
|
71
|
+
payload_len = b2 & 0x7f
|
|
72
|
+
|
|
73
|
+
payload_len = read_exact(2).unpack1("n") if payload_len == 126
|
|
74
|
+
payload_len = read_exact(8).unpack1("Q>") if payload_len == 127
|
|
75
|
+
|
|
76
|
+
masking_key = masked ? read_exact(4) : nil
|
|
77
|
+
payload = payload_len.zero? ? "".b : read_exact(payload_len)
|
|
78
|
+
return nil if payload.nil?
|
|
79
|
+
|
|
80
|
+
payload = unmask(payload, masking_key) if masked
|
|
81
|
+
|
|
82
|
+
{ opcode: b1 & 0x0f, payload: payload }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def send_frame(opcode, payload)
|
|
86
|
+
bytes = payload.to_s.b
|
|
87
|
+
len = bytes.bytesize
|
|
88
|
+
header = [0x80 | (opcode & 0x0f)].pack("C")
|
|
89
|
+
|
|
90
|
+
header <<
|
|
91
|
+
if len <= 125
|
|
92
|
+
[len].pack("C")
|
|
93
|
+
elsif len <= 0xffff
|
|
94
|
+
[126].pack("C") + [len].pack("n")
|
|
95
|
+
else
|
|
96
|
+
[127].pack("C") + [len].pack("Q>")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
@write_mutex.synchronize do
|
|
100
|
+
@socket.write(header)
|
|
101
|
+
@socket.write(bytes) unless bytes.empty?
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def unmask(payload, mask)
|
|
106
|
+
out = +""
|
|
107
|
+
out.force_encoding(Encoding::BINARY)
|
|
108
|
+
payload.bytes.each_with_index do |byte, idx|
|
|
109
|
+
out << (byte ^ mask.getbyte(idx % 4))
|
|
110
|
+
end
|
|
111
|
+
out
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def read_exact(length)
|
|
115
|
+
chunk = +""
|
|
116
|
+
chunk.force_encoding(Encoding::BINARY)
|
|
117
|
+
|
|
118
|
+
while chunk.bytesize < length
|
|
119
|
+
part = @socket.read(length - chunk.bytesize)
|
|
120
|
+
return nil if part.nil? || part.empty?
|
|
121
|
+
|
|
122
|
+
chunk << part
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
chunk
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruflet
|
|
4
|
+
class WireCodec
|
|
5
|
+
class << self
|
|
6
|
+
def pack(value)
|
|
7
|
+
case value
|
|
8
|
+
when NilClass
|
|
9
|
+
"\xc0".b
|
|
10
|
+
when TrueClass
|
|
11
|
+
"\xc3".b
|
|
12
|
+
when FalseClass
|
|
13
|
+
"\xc2".b
|
|
14
|
+
when Integer
|
|
15
|
+
pack_integer(value)
|
|
16
|
+
when Float
|
|
17
|
+
"\xcb".b + [value].pack("G")
|
|
18
|
+
when String
|
|
19
|
+
pack_string(value)
|
|
20
|
+
when Symbol
|
|
21
|
+
pack_string(value.to_s)
|
|
22
|
+
when Array
|
|
23
|
+
pack_array(value)
|
|
24
|
+
when Hash
|
|
25
|
+
pack_map(value)
|
|
26
|
+
else
|
|
27
|
+
pack_string(value.to_s)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def unpack(bytes)
|
|
32
|
+
reader = ByteReader.new(bytes)
|
|
33
|
+
read_value(reader)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def pack_integer(value)
|
|
39
|
+
if value >= 0
|
|
40
|
+
return [value].pack("C") if value <= 0x7f
|
|
41
|
+
return "\xcc".b + [value].pack("C") if value <= 0xff
|
|
42
|
+
return "\xcd".b + [value].pack("n") if value <= 0xffff
|
|
43
|
+
return "\xce".b + [value].pack("N") if value <= 0xffff_ffff
|
|
44
|
+
|
|
45
|
+
"\xcf".b + [value].pack("Q>")
|
|
46
|
+
else
|
|
47
|
+
return [value & 0xff].pack("C") if value >= -32
|
|
48
|
+
return "\xd0".b + [value].pack("c") if value >= -128
|
|
49
|
+
return "\xd1".b + [value].pack("s>") if value >= -32_768
|
|
50
|
+
return "\xd2".b + [value].pack("l>") if value >= -2_147_483_648
|
|
51
|
+
|
|
52
|
+
"\xd3".b + [value].pack("q>")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def pack_string(value)
|
|
57
|
+
str = value.to_s.dup.force_encoding("UTF-8")
|
|
58
|
+
bytes = str.b
|
|
59
|
+
len = bytes.bytesize
|
|
60
|
+
|
|
61
|
+
if len <= 31
|
|
62
|
+
[0xA0 | len].pack("C") + bytes
|
|
63
|
+
elsif len <= 0xff
|
|
64
|
+
"\xd9".b + [len].pack("C") + bytes
|
|
65
|
+
elsif len <= 0xffff
|
|
66
|
+
"\xda".b + [len].pack("n") + bytes
|
|
67
|
+
else
|
|
68
|
+
"\xdb".b + [len].pack("N") + bytes
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def pack_array(value)
|
|
73
|
+
len = value.length
|
|
74
|
+
head =
|
|
75
|
+
if len <= 15
|
|
76
|
+
[0x90 | len].pack("C")
|
|
77
|
+
elsif len <= 0xffff
|
|
78
|
+
"\xdc".b + [len].pack("n")
|
|
79
|
+
else
|
|
80
|
+
"\xdd".b + [len].pack("N")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
body = +"".b
|
|
84
|
+
value.each { |item| body << pack(item) }
|
|
85
|
+
head + body
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def pack_map(value)
|
|
89
|
+
pairs = value.each_with_object({}) { |(k, v), out| out[k.to_s] = v }
|
|
90
|
+
len = pairs.length
|
|
91
|
+
head =
|
|
92
|
+
if len <= 15
|
|
93
|
+
[0x80 | len].pack("C")
|
|
94
|
+
elsif len <= 0xffff
|
|
95
|
+
"\xde".b + [len].pack("n")
|
|
96
|
+
else
|
|
97
|
+
"\xdf".b + [len].pack("N")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
body = +"".b
|
|
101
|
+
pairs.each do |k, v|
|
|
102
|
+
body << pack(k)
|
|
103
|
+
body << pack(v)
|
|
104
|
+
end
|
|
105
|
+
head + body
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def read_value(reader)
|
|
109
|
+
marker = reader.read_u8
|
|
110
|
+
|
|
111
|
+
return marker if marker <= 0x7f
|
|
112
|
+
return marker - 256 if marker >= 0xe0
|
|
113
|
+
|
|
114
|
+
case marker
|
|
115
|
+
when 0xc0 then nil
|
|
116
|
+
when 0xc2 then false
|
|
117
|
+
when 0xc3 then true
|
|
118
|
+
when 0xcc then reader.read_u8
|
|
119
|
+
when 0xcd then reader.read_u16
|
|
120
|
+
when 0xce then reader.read_u32
|
|
121
|
+
when 0xcf then reader.read_u64
|
|
122
|
+
when 0xd0 then reader.read_i8
|
|
123
|
+
when 0xd1 then reader.read_i16
|
|
124
|
+
when 0xd2 then reader.read_i32
|
|
125
|
+
when 0xd3 then reader.read_i64
|
|
126
|
+
when 0xca then reader.read_f32
|
|
127
|
+
when 0xcb then reader.read_f64
|
|
128
|
+
when 0xd9 then reader.read_string(reader.read_u8)
|
|
129
|
+
when 0xda then reader.read_string(reader.read_u16)
|
|
130
|
+
when 0xdb then reader.read_string(reader.read_u32)
|
|
131
|
+
when 0xdc then read_array(reader, reader.read_u16)
|
|
132
|
+
when 0xdd then read_array(reader, reader.read_u32)
|
|
133
|
+
when 0xde then read_map(reader, reader.read_u16)
|
|
134
|
+
when 0xdf then read_map(reader, reader.read_u32)
|
|
135
|
+
else
|
|
136
|
+
if (marker & 0xf0) == 0x90
|
|
137
|
+
read_array(reader, marker & 0x0f)
|
|
138
|
+
elsif (marker & 0xf0) == 0x80
|
|
139
|
+
read_map(reader, marker & 0x0f)
|
|
140
|
+
elsif (marker & 0xe0) == 0xa0
|
|
141
|
+
reader.read_string(marker & 0x1f)
|
|
142
|
+
else
|
|
143
|
+
raise "Unsupported MessagePack marker: 0x#{marker.to_s(16)}"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def read_array(reader, size)
|
|
149
|
+
Array.new(size) { read_value(reader) }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def read_map(reader, size)
|
|
153
|
+
out = {}
|
|
154
|
+
size.times do
|
|
155
|
+
key = read_value(reader)
|
|
156
|
+
out[key.to_s] = read_value(reader)
|
|
157
|
+
end
|
|
158
|
+
out
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
class ByteReader
|
|
163
|
+
def initialize(bytes)
|
|
164
|
+
@data = bytes.to_s.b
|
|
165
|
+
@offset = 0
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def read_u8
|
|
169
|
+
value = @data.getbyte(@offset)
|
|
170
|
+
raise "Unexpected EOF" if value.nil?
|
|
171
|
+
|
|
172
|
+
@offset += 1
|
|
173
|
+
value
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def read_exact(size)
|
|
177
|
+
chunk = @data.byteslice(@offset, size)
|
|
178
|
+
raise "Unexpected EOF" if chunk.nil? || chunk.bytesize != size
|
|
179
|
+
|
|
180
|
+
@offset += size
|
|
181
|
+
chunk
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def read_u16
|
|
185
|
+
read_exact(2).unpack1("n")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def read_u32
|
|
189
|
+
read_exact(4).unpack1("N")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def read_u64
|
|
193
|
+
read_exact(8).unpack1("Q>")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def read_i8
|
|
197
|
+
read_exact(1).unpack1("c")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def read_i16
|
|
201
|
+
read_exact(2).unpack1("s>")
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def read_i32
|
|
205
|
+
read_exact(4).unpack1("l>")
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def read_i64
|
|
209
|
+
read_exact(8).unpack1("q>")
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def read_f32
|
|
213
|
+
read_exact(4).unpack1("g")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def read_f64
|
|
217
|
+
read_exact(8).unpack1("G")
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def read_string(size)
|
|
221
|
+
read_exact(size).force_encoding("UTF-8")
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest/sha1"
|
|
4
|
+
require "socket"
|
|
5
|
+
require "thread"
|
|
6
|
+
|
|
7
|
+
require "ruflet"
|
|
8
|
+
require_relative "server/wire_codec"
|
|
9
|
+
require_relative "server/web_socket_connection"
|
|
10
|
+
|
|
11
|
+
module Ruflet
|
|
12
|
+
class Server
|
|
13
|
+
attr_reader :port
|
|
14
|
+
|
|
15
|
+
WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
16
|
+
|
|
17
|
+
def initialize(host: "0.0.0.0", port: 8550, &app_block)
|
|
18
|
+
@host = host
|
|
19
|
+
@port = port
|
|
20
|
+
@app_block = app_block
|
|
21
|
+
@sessions = {}
|
|
22
|
+
@sessions_mutex = Mutex.new
|
|
23
|
+
@connections = {}
|
|
24
|
+
@connections_mutex = Mutex.new
|
|
25
|
+
@running = false
|
|
26
|
+
@server_socket = nil
|
|
27
|
+
|
|
28
|
+
at_exit do
|
|
29
|
+
begin
|
|
30
|
+
stop
|
|
31
|
+
rescue StandardError
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def start
|
|
38
|
+
previous_signals = trap_stop_signals
|
|
39
|
+
bind_server_socket!
|
|
40
|
+
@running = true
|
|
41
|
+
print_server_banner
|
|
42
|
+
accept_loop
|
|
43
|
+
rescue Interrupt
|
|
44
|
+
nil
|
|
45
|
+
ensure
|
|
46
|
+
stop
|
|
47
|
+
restore_stop_signals(previous_signals)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# For Rack-hosted mode: caller already performed the HTTP upgrade.
|
|
51
|
+
def handle_upgraded_socket(io)
|
|
52
|
+
ws = Ruflet::WebSocketConnection.new(io)
|
|
53
|
+
run_connection(ws)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def bind_server_socket!(max_attempts: 100)
|
|
57
|
+
requested = @port.to_i
|
|
58
|
+
candidate = requested
|
|
59
|
+
|
|
60
|
+
max_attempts.times do
|
|
61
|
+
begin
|
|
62
|
+
@server_socket = TCPServer.new(@host, candidate)
|
|
63
|
+
@port = candidate
|
|
64
|
+
warn "Requested port #{requested} is busy; bound to #{@port}" if @port != requested
|
|
65
|
+
return
|
|
66
|
+
rescue Errno::EADDRINUSE
|
|
67
|
+
candidate += 1
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
raise Errno::EADDRINUSE, "Unable to bind starting at #{requested} after #{max_attempts} attempts"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def stop
|
|
75
|
+
return unless @running || @server_socket
|
|
76
|
+
|
|
77
|
+
@running = false
|
|
78
|
+
|
|
79
|
+
server = @server_socket
|
|
80
|
+
@server_socket = nil
|
|
81
|
+
begin
|
|
82
|
+
server&.close
|
|
83
|
+
rescue IOError
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
live_connections = @connections_mutex.synchronize { @connections.values.dup }
|
|
88
|
+
live_connections.each do |conn|
|
|
89
|
+
begin
|
|
90
|
+
conn.close
|
|
91
|
+
rescue StandardError
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def trap_stop_signals
|
|
100
|
+
{
|
|
101
|
+
"INT" => trap_signal("INT"),
|
|
102
|
+
"TERM" => trap_signal("TERM")
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def trap_signal(signal_name)
|
|
107
|
+
Signal.trap(signal_name) do
|
|
108
|
+
stop
|
|
109
|
+
Thread.main.raise(Interrupt)
|
|
110
|
+
rescue StandardError
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def restore_stop_signals(previous_signals)
|
|
116
|
+
return unless previous_signals
|
|
117
|
+
|
|
118
|
+
previous_signals.each do |signal_name, handler|
|
|
119
|
+
Signal.trap(signal_name, handler) if handler
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def print_server_banner
|
|
124
|
+
return if ENV["RUFLET_SUPPRESS_SERVER_BANNER"] == "1"
|
|
125
|
+
|
|
126
|
+
warn "Ruflet server listening on ws://#{@host}:#{@port}/ws"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def accept_loop
|
|
130
|
+
while @running
|
|
131
|
+
socket = accept_client_socket
|
|
132
|
+
break unless socket
|
|
133
|
+
|
|
134
|
+
start_client_thread(socket)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def accept_client_socket
|
|
139
|
+
accepted = @server_socket.accept
|
|
140
|
+
accepted.is_a?(Array) ? accepted.first : accepted
|
|
141
|
+
rescue IOError, Errno::EBADF
|
|
142
|
+
nil
|
|
143
|
+
rescue StandardError => e
|
|
144
|
+
warn "accept error: #{e.class}: #{e.message}"
|
|
145
|
+
warn e.backtrace.join("\n") if e.backtrace
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def start_client_thread(socket)
|
|
150
|
+
Thread.new(socket) do |client|
|
|
151
|
+
Thread.current.report_on_exception = false if Thread.current.respond_to?(:report_on_exception=)
|
|
152
|
+
handle_socket(client)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def handle_socket(socket)
|
|
157
|
+
ws = nil
|
|
158
|
+
begin
|
|
159
|
+
path, headers = read_http_upgrade_request(socket)
|
|
160
|
+
return unless websocket_upgrade_request?(path, headers)
|
|
161
|
+
|
|
162
|
+
send_handshake_response(socket, headers["sec-websocket-key"])
|
|
163
|
+
ws = Ruflet::WebSocketConnection.new(socket)
|
|
164
|
+
run_connection(ws)
|
|
165
|
+
rescue StandardError => e
|
|
166
|
+
warn "server error: #{e.class}: #{e.message}"
|
|
167
|
+
warn e.backtrace.join("\n") if e.backtrace
|
|
168
|
+
send_message(ws, Protocol::ACTIONS[:session_crashed], { "message" => e.message.to_s.dup.force_encoding("UTF-8") }) if ws
|
|
169
|
+
ensure
|
|
170
|
+
close_connection(ws)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def run_connection(ws)
|
|
175
|
+
register_connection(ws)
|
|
176
|
+
|
|
177
|
+
while (raw = ws.read_message)
|
|
178
|
+
handle_message(ws, raw)
|
|
179
|
+
end
|
|
180
|
+
rescue StandardError => e
|
|
181
|
+
warn "server error: #{e.class}: #{e.message}"
|
|
182
|
+
warn e.backtrace.join("\n") if e.backtrace
|
|
183
|
+
send_message(ws, Protocol::ACTIONS[:session_crashed], { "message" => e.message.to_s.dup.force_encoding("UTF-8") })
|
|
184
|
+
ensure
|
|
185
|
+
close_connection(ws)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def close_connection(ws)
|
|
189
|
+
remove_session(ws)
|
|
190
|
+
unregister_connection(ws)
|
|
191
|
+
ws&.close
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def read_http_upgrade_request(socket)
|
|
195
|
+
request_line = socket.gets("\r\n")
|
|
196
|
+
raise "Invalid HTTP request" if request_line.nil?
|
|
197
|
+
|
|
198
|
+
method, path, _version = request_line.strip.split(" ", 3)
|
|
199
|
+
raise "Unsupported HTTP method: #{method}" unless method == "GET"
|
|
200
|
+
|
|
201
|
+
headers = {}
|
|
202
|
+
loop do
|
|
203
|
+
line = socket.gets("\r\n")
|
|
204
|
+
break if line.nil? || line == "\r\n"
|
|
205
|
+
|
|
206
|
+
key, value = line.split(":", 2)
|
|
207
|
+
next if key.nil? || value.nil?
|
|
208
|
+
|
|
209
|
+
headers[key.strip.downcase] = value.strip
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
[path, headers]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def websocket_upgrade_request?(path, headers)
|
|
216
|
+
return false unless path == "/ws"
|
|
217
|
+
return false unless headers["upgrade"]&.downcase == "websocket"
|
|
218
|
+
return false unless headers["connection"]&.downcase&.include?("upgrade")
|
|
219
|
+
return false if headers["sec-websocket-key"].to_s.empty?
|
|
220
|
+
|
|
221
|
+
true
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def send_handshake_response(socket, key)
|
|
225
|
+
accept = [Digest::SHA1.digest("#{key}#{WEBSOCKET_GUID}")].pack("m0")
|
|
226
|
+
|
|
227
|
+
socket.write("HTTP/1.1 101 Switching Protocols\r\n")
|
|
228
|
+
socket.write("Upgrade: websocket\r\n")
|
|
229
|
+
socket.write("Connection: Upgrade\r\n")
|
|
230
|
+
socket.write("Sec-WebSocket-Accept: #{accept}\r\n")
|
|
231
|
+
socket.write("\r\n")
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def remove_session(ws)
|
|
235
|
+
return unless ws
|
|
236
|
+
|
|
237
|
+
@sessions_mutex.synchronize do
|
|
238
|
+
@sessions.delete(ws.session_key)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def register_connection(ws)
|
|
243
|
+
return unless ws
|
|
244
|
+
|
|
245
|
+
@connections_mutex.synchronize do
|
|
246
|
+
@connections[ws.session_key] = ws
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def unregister_connection(ws)
|
|
251
|
+
return unless ws
|
|
252
|
+
|
|
253
|
+
@connections_mutex.synchronize do
|
|
254
|
+
@connections.delete(ws.session_key)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def handle_message(ws, raw)
|
|
259
|
+
action, payload = decode_incoming(raw)
|
|
260
|
+
payload ||= {}
|
|
261
|
+
|
|
262
|
+
warn "incoming action=#{action.inspect}" if ENV["FLET_DEBUG"] == "1"
|
|
263
|
+
|
|
264
|
+
case action
|
|
265
|
+
when Protocol::ACTIONS[:register_client], Protocol::ACTIONS[:register_web_client]
|
|
266
|
+
on_register_client(ws, payload)
|
|
267
|
+
when Protocol::ACTIONS[:control_event], Protocol::ACTIONS[:page_event_from_web]
|
|
268
|
+
on_control_event(ws, payload)
|
|
269
|
+
when Protocol::ACTIONS[:update_control], Protocol::ACTIONS[:update_control_props]
|
|
270
|
+
on_update_control(ws, payload)
|
|
271
|
+
else
|
|
272
|
+
raise "Unknown action: #{action.inspect}"
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def decode_incoming(raw)
|
|
277
|
+
parsed = normalize_incoming(Ruflet::WireCodec.unpack(raw.to_s.b))
|
|
278
|
+
|
|
279
|
+
if parsed.is_a?(Array) && parsed.length >= 2
|
|
280
|
+
return [parsed[0], parsed[1]]
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
if parsed.is_a?(Hash)
|
|
284
|
+
action = parsed["action"] || parsed[:action]
|
|
285
|
+
payload = parsed["payload"] || parsed[:payload]
|
|
286
|
+
return [action, payload] unless action.nil?
|
|
287
|
+
|
|
288
|
+
if (parsed.key?("target") || parsed.key?(:target)) && (parsed.key?("name") || parsed.key?(:name))
|
|
289
|
+
return [Protocol::ACTIONS[:control_event], parsed]
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
raise "Unsupported payload format"
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def normalize_incoming(value)
|
|
297
|
+
case value
|
|
298
|
+
when String
|
|
299
|
+
value.dup.force_encoding("UTF-8")
|
|
300
|
+
when Integer, Float, TrueClass, FalseClass, NilClass
|
|
301
|
+
value
|
|
302
|
+
when Symbol
|
|
303
|
+
value.to_s
|
|
304
|
+
when Array
|
|
305
|
+
value.map { |v| normalize_incoming(v) }
|
|
306
|
+
when Hash
|
|
307
|
+
value.each_with_object({}) do |(k, v), out|
|
|
308
|
+
out[k.to_s] = normalize_incoming(v)
|
|
309
|
+
end
|
|
310
|
+
else
|
|
311
|
+
value.to_s
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def on_register_client(ws, payload)
|
|
316
|
+
normalized = Protocol.normalize_register_payload(payload)
|
|
317
|
+
session_id = normalized["session_id"].to_s.empty? ? pseudo_uuid : normalized["session_id"]
|
|
318
|
+
|
|
319
|
+
page = Page.new(
|
|
320
|
+
session_id: session_id,
|
|
321
|
+
client_details: normalized,
|
|
322
|
+
sender: lambda do |action, msg_payload|
|
|
323
|
+
send_message(ws, action, msg_payload)
|
|
324
|
+
end
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
page.title = "Ruflet App"
|
|
328
|
+
|
|
329
|
+
@sessions_mutex.synchronize do
|
|
330
|
+
@sessions[ws.session_key] = page
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
initial_response = [
|
|
334
|
+
Protocol::ACTIONS[:register_client],
|
|
335
|
+
{
|
|
336
|
+
"session_id" => session_id,
|
|
337
|
+
"page_patch" => {},
|
|
338
|
+
"error" => nil
|
|
339
|
+
}
|
|
340
|
+
]
|
|
341
|
+
ws.send_binary(Ruflet::WireCodec.pack(initial_response))
|
|
342
|
+
|
|
343
|
+
@app_block.call(page)
|
|
344
|
+
page.update
|
|
345
|
+
rescue StandardError => e
|
|
346
|
+
send_message(ws, Protocol::ACTIONS[:session_crashed], { "message" => e.message })
|
|
347
|
+
raise
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def on_control_event(ws, payload)
|
|
351
|
+
event = Protocol.normalize_control_event_payload(payload)
|
|
352
|
+
page = fetch_page(ws)
|
|
353
|
+
return if event["target"].nil? || event["name"].to_s.empty?
|
|
354
|
+
|
|
355
|
+
page.dispatch_event(
|
|
356
|
+
target: event["target"],
|
|
357
|
+
name: event["name"],
|
|
358
|
+
data: normalize_event_data(event["data"])
|
|
359
|
+
)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def on_update_control(ws, payload)
|
|
363
|
+
update = Protocol.normalize_update_control_payload(payload)
|
|
364
|
+
page = fetch_page(ws)
|
|
365
|
+
return if update["id"].nil?
|
|
366
|
+
|
|
367
|
+
page.apply_client_update(update["id"], update["props"] || {})
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def fetch_page(ws)
|
|
371
|
+
page = @sessions_mutex.synchronize { @sessions[ws.session_key] }
|
|
372
|
+
raise "Session not found" unless page
|
|
373
|
+
|
|
374
|
+
page
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def normalize_event_data(value)
|
|
378
|
+
case value
|
|
379
|
+
when Hash
|
|
380
|
+
value.each_with_object({}) { |(k, v), out| out[k.to_sym] = normalize_event_data(v) }
|
|
381
|
+
when Array
|
|
382
|
+
value.map { |entry| normalize_event_data(entry) }
|
|
383
|
+
else
|
|
384
|
+
value
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def send_message(ws, action, payload)
|
|
389
|
+
message = [action, payload]
|
|
390
|
+
ws.send_binary(Ruflet::WireCodec.pack(message))
|
|
391
|
+
rescue StandardError => e
|
|
392
|
+
warn "send error: #{e.class}: #{e.message}"
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def pseudo_uuid
|
|
396
|
+
now = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
|
|
397
|
+
rnd = rand(0..0xffff_ffff)
|
|
398
|
+
"%08x-%04x-%04x-%04x-%012x" % [
|
|
399
|
+
rnd,
|
|
400
|
+
now & 0xffff,
|
|
401
|
+
(now >> 16) & 0xffff,
|
|
402
|
+
(now >> 32) & 0xffff,
|
|
403
|
+
(now >> 48) & 0xffff_ffff_ffff
|
|
404
|
+
]
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruflet"
|
|
4
|
+
require_relative "ruflet/server"
|
|
5
|
+
|
|
6
|
+
module Ruflet
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def run(entrypoint = nil, host: "0.0.0.0", port: 8550, &block)
|
|
10
|
+
callback = entrypoint || block
|
|
11
|
+
raise ArgumentError, "Ruflet.run requires a callable entrypoint or block" unless callback.respond_to?(:call)
|
|
12
|
+
|
|
13
|
+
Server.new(host: host, port: port) do |page|
|
|
14
|
+
callback.call(page)
|
|
15
|
+
end.start
|
|
16
|
+
end
|
|
17
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ruflet_server
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- AdamMusa
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ruflet
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - '='
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.0.1
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - '='
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.0.1
|
|
26
|
+
description: Ruflet WebSocket server runtime compatible with Flet protocol.
|
|
27
|
+
email:
|
|
28
|
+
- adammusa2222@gmail.com
|
|
29
|
+
executables: []
|
|
30
|
+
extensions: []
|
|
31
|
+
extra_rdoc_files: []
|
|
32
|
+
files:
|
|
33
|
+
- README.md
|
|
34
|
+
- lib/ruflet/server.rb
|
|
35
|
+
- lib/ruflet/server/web_socket_connection.rb
|
|
36
|
+
- lib/ruflet/server/wire_codec.rb
|
|
37
|
+
- lib/ruflet/version.rb
|
|
38
|
+
- lib/ruflet_server.rb
|
|
39
|
+
homepage: https://github.com/AdamMusa/Ruflet
|
|
40
|
+
licenses: []
|
|
41
|
+
metadata: {}
|
|
42
|
+
rdoc_options: []
|
|
43
|
+
require_paths:
|
|
44
|
+
- lib
|
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '3.1'
|
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
requirements: []
|
|
56
|
+
rubygems_version: 3.7.2
|
|
57
|
+
specification_version: 4
|
|
58
|
+
summary: Ruflet server package.
|
|
59
|
+
test_files: []
|