eui-ruby 0.1.0
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/CHANGELOG.md +31 -0
- data/LICENSE +21 -0
- data/README.md +192 -0
- data/examples/counter.rb +65 -0
- data/lib/eui/app.rb +83 -0
- data/lib/eui/assets.rb +76 -0
- data/lib/eui/blake3.rb +216 -0
- data/lib/eui/component.rb +105 -0
- data/lib/eui/dsl.rb +166 -0
- data/lib/eui/errors.rb +19 -0
- data/lib/eui/manifest.rb +109 -0
- data/lib/eui/proto/frame.rb +243 -0
- data/lib/eui/proto/limits.rb +47 -0
- data/lib/eui/proto/node.rb +385 -0
- data/lib/eui/proto/op.rb +196 -0
- data/lib/eui/proto/reader.rb +125 -0
- data/lib/eui/proto/style.rb +250 -0
- data/lib/eui/proto/writer.rb +89 -0
- data/lib/eui/proto.rb +22 -0
- data/lib/eui/server.rb +175 -0
- data/lib/eui/session.rb +289 -0
- data/lib/eui/theme.rb +73 -0
- data/lib/eui/version.rb +5 -0
- data/lib/eui/view/diff.rb +212 -0
- data/lib/eui/view/style.rb +198 -0
- data/lib/eui/view/tree.rb +378 -0
- data/lib/eui/websocket.rb +195 -0
- data/lib/eui-ruby.rb +4 -0
- data/lib/eui.rb +32 -0
- metadata +80 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest/sha1'
|
|
4
|
+
require 'base64'
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
require_relative 'errors'
|
|
7
|
+
require_relative 'proto'
|
|
8
|
+
|
|
9
|
+
module EUI
|
|
10
|
+
# The server half of RFC 6455, with only what an EUI session needs.
|
|
11
|
+
#
|
|
12
|
+
# A session carries **binary** frames and nothing else: a text frame is
|
|
13
|
+
# not a protocol extension point, it is a sign that something other than
|
|
14
|
+
# an EUI client is talking, and the session ends
|
|
15
|
+
# (`spec/01-transport.md` §2.3).
|
|
16
|
+
class WebSocket
|
|
17
|
+
GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
|
18
|
+
MAX_MESSAGE = Proto::Limits::MAX_FRAME_BYTES + 16
|
|
19
|
+
|
|
20
|
+
CONTINUATION = 0x0
|
|
21
|
+
TEXT = 0x1
|
|
22
|
+
BINARY = 0x2
|
|
23
|
+
CLOSE = 0x8
|
|
24
|
+
PING = 0x9
|
|
25
|
+
PONG = 0xA
|
|
26
|
+
|
|
27
|
+
class ClosedError < Error; end
|
|
28
|
+
class ProtocolError < Error; end
|
|
29
|
+
|
|
30
|
+
attr_reader :path, :headers
|
|
31
|
+
|
|
32
|
+
def initialize(socket, path:, headers:)
|
|
33
|
+
@socket = socket
|
|
34
|
+
@path = path
|
|
35
|
+
@headers = headers
|
|
36
|
+
@write_lock = Mutex.new
|
|
37
|
+
@closed = false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Answer the upgrade. The key is not a secret and proves nothing: it is
|
|
41
|
+
# there so that a cache between the two ends cannot mistake this for a
|
|
42
|
+
# reply it may serve to somebody else.
|
|
43
|
+
def self.accept(socket, path:, headers:)
|
|
44
|
+
key = headers['sec-websocket-key']
|
|
45
|
+
raise ProtocolError, 'no Sec-WebSocket-Key' unless key
|
|
46
|
+
|
|
47
|
+
accept = Base64.strict_encode64(Digest::SHA1.digest(key + GUID))
|
|
48
|
+
socket.write(
|
|
49
|
+
"HTTP/1.1 101 Switching Protocols\r\n" \
|
|
50
|
+
"Upgrade: websocket\r\n" \
|
|
51
|
+
"Connection: Upgrade\r\n" \
|
|
52
|
+
"Sec-WebSocket-Accept: #{accept}\r\n\r\n"
|
|
53
|
+
)
|
|
54
|
+
new(socket, path: path, headers: headers)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def closed? = @closed
|
|
58
|
+
|
|
59
|
+
# The next application message, or nil once the peer has gone.
|
|
60
|
+
# Control frames are answered here and never surface.
|
|
61
|
+
def recv
|
|
62
|
+
message = +''
|
|
63
|
+
message.force_encoding(Encoding::BINARY)
|
|
64
|
+
kind = nil
|
|
65
|
+
|
|
66
|
+
loop do
|
|
67
|
+
frame = read_frame
|
|
68
|
+
return nil if frame.nil?
|
|
69
|
+
|
|
70
|
+
opcode, payload, fin = frame
|
|
71
|
+
case opcode
|
|
72
|
+
when CLOSE
|
|
73
|
+
send_close(1000)
|
|
74
|
+
return nil
|
|
75
|
+
when PING
|
|
76
|
+
send_frame(PONG, payload)
|
|
77
|
+
next
|
|
78
|
+
when PONG
|
|
79
|
+
next
|
|
80
|
+
when TEXT, BINARY
|
|
81
|
+
raise ProtocolError, 'a frame arrived inside a fragmented message' unless message.empty?
|
|
82
|
+
|
|
83
|
+
kind = opcode
|
|
84
|
+
message << payload
|
|
85
|
+
when CONTINUATION
|
|
86
|
+
raise ProtocolError, 'a continuation with nothing to continue' if kind.nil?
|
|
87
|
+
|
|
88
|
+
message << payload
|
|
89
|
+
else
|
|
90
|
+
raise ProtocolError, "unknown opcode #{opcode}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
raise ProtocolError, 'message too large' if message.bytesize > MAX_MESSAGE
|
|
94
|
+
next unless fin
|
|
95
|
+
|
|
96
|
+
return [kind == TEXT ? :text : :binary, message]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def send_binary(data) = send_frame(BINARY, data)
|
|
101
|
+
|
|
102
|
+
def send_close(code = 1000, reason = '')
|
|
103
|
+
return if @closed
|
|
104
|
+
|
|
105
|
+
payload = [code].pack('n') + reason.to_s.byteslice(0, 123).to_s
|
|
106
|
+
send_frame(CLOSE, payload)
|
|
107
|
+
@closed = true
|
|
108
|
+
rescue Error, SystemCallError, IOError
|
|
109
|
+
@closed = true
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def close
|
|
113
|
+
send_close
|
|
114
|
+
@socket.close
|
|
115
|
+
rescue SystemCallError, IOError
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def send_frame(opcode, payload)
|
|
122
|
+
payload = payload.to_s.b
|
|
123
|
+
header = +''
|
|
124
|
+
header.force_encoding(Encoding::BINARY)
|
|
125
|
+
header << (0x80 | opcode).chr
|
|
126
|
+
len = payload.bytesize
|
|
127
|
+
if len < 126
|
|
128
|
+
header << len.chr
|
|
129
|
+
elsif len < 65_536
|
|
130
|
+
header << 126.chr << [len].pack('n')
|
|
131
|
+
else
|
|
132
|
+
header << 127.chr << [len].pack('Q>')
|
|
133
|
+
end
|
|
134
|
+
@write_lock.synchronize do
|
|
135
|
+
raise ClosedError, 'the socket is closed' if @socket.closed?
|
|
136
|
+
|
|
137
|
+
@socket.write(header, payload)
|
|
138
|
+
end
|
|
139
|
+
rescue SystemCallError, IOError => e
|
|
140
|
+
@closed = true
|
|
141
|
+
raise ClosedError, e.message
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def read_frame
|
|
145
|
+
head = read_exactly(2)
|
|
146
|
+
return nil if head.nil?
|
|
147
|
+
|
|
148
|
+
b0, b1 = head.bytes
|
|
149
|
+
fin = (b0 & 0x80) != 0
|
|
150
|
+
raise ProtocolError, 'reserved bits set' if (b0 & 0x70) != 0
|
|
151
|
+
|
|
152
|
+
opcode = b0 & 0x0F
|
|
153
|
+
masked = (b1 & 0x80) != 0
|
|
154
|
+
# Every frame from a client is masked; one that is not is either a
|
|
155
|
+
# proxy rewriting traffic or something that is not a browser stack.
|
|
156
|
+
raise ProtocolError, 'a client frame must be masked' unless masked
|
|
157
|
+
|
|
158
|
+
len = b1 & 0x7F
|
|
159
|
+
len = read_exactly(2).unpack1('n') if len == 126
|
|
160
|
+
len = read_exactly(8).unpack1('Q>') if len == 127
|
|
161
|
+
raise ProtocolError, 'frame too large' if len > MAX_MESSAGE
|
|
162
|
+
|
|
163
|
+
mask = read_exactly(4)
|
|
164
|
+
payload = len.zero? ? +'' : read_exactly(len)
|
|
165
|
+
return nil if payload.nil?
|
|
166
|
+
|
|
167
|
+
[opcode, unmask(payload, mask), fin]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def unmask(payload, mask)
|
|
171
|
+
return payload if payload.empty?
|
|
172
|
+
|
|
173
|
+
key = mask.bytes
|
|
174
|
+
out = payload.dup
|
|
175
|
+
out.force_encoding(Encoding::BINARY)
|
|
176
|
+
bytes = out.bytes
|
|
177
|
+
bytes.each_with_index { |b, i| bytes[i] = b ^ key[i & 3] }
|
|
178
|
+
bytes.pack('C*')
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def read_exactly(count)
|
|
182
|
+
data = +''
|
|
183
|
+
data.force_encoding(Encoding::BINARY)
|
|
184
|
+
while data.bytesize < count
|
|
185
|
+
chunk = @socket.read(count - data.bytesize)
|
|
186
|
+
return nil if chunk.nil? || chunk.empty?
|
|
187
|
+
|
|
188
|
+
data << chunk
|
|
189
|
+
end
|
|
190
|
+
data
|
|
191
|
+
rescue SystemCallError, IOError
|
|
192
|
+
nil
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
data/lib/eui-ruby.rb
ADDED
data/lib/eui.rb
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'eui/version'
|
|
4
|
+
require_relative 'eui/errors'
|
|
5
|
+
require_relative 'eui/blake3'
|
|
6
|
+
require_relative 'eui/theme'
|
|
7
|
+
require_relative 'eui/proto'
|
|
8
|
+
require_relative 'eui/view/style'
|
|
9
|
+
require_relative 'eui/view/tree'
|
|
10
|
+
require_relative 'eui/view/diff'
|
|
11
|
+
require_relative 'eui/dsl'
|
|
12
|
+
require_relative 'eui/assets'
|
|
13
|
+
require_relative 'eui/manifest'
|
|
14
|
+
require_relative 'eui/component'
|
|
15
|
+
require_relative 'eui/websocket'
|
|
16
|
+
require_relative 'eui/session'
|
|
17
|
+
require_relative 'eui/server'
|
|
18
|
+
require_relative 'eui/app'
|
|
19
|
+
|
|
20
|
+
# EUI in Ruby: an application interface delivered over HTTPS without HTML,
|
|
21
|
+
# CSS or JavaScript.
|
|
22
|
+
#
|
|
23
|
+
# The server sends an interface tree that is **already resolved**, in a
|
|
24
|
+
# compact binary encoding; a native client applies it, lays it out and draws
|
|
25
|
+
# it on the GPU. There is no tolerant parse, no cascade to resolve and no
|
|
26
|
+
# script to run at the other end — which is why a view here is a hash and a
|
|
27
|
+
# style is a flat, closed vocabulary rather than a language.
|
|
28
|
+
#
|
|
29
|
+
# The protocol is specified in `spec/` of the EUI repository; every file in
|
|
30
|
+
# this gem names the section it implements.
|
|
31
|
+
module EUI
|
|
32
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: eui-ruby
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Olivier Bonnaure
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: |
|
|
13
|
+
EUI delivers an application interface over HTTPS without HTML, CSS or
|
|
14
|
+
JavaScript: the server sends a tree that is already resolved, in a
|
|
15
|
+
compact binary encoding, and a native client lays it out and draws it on
|
|
16
|
+
the GPU. This gem is the server half in Ruby — the encoder, the session,
|
|
17
|
+
the content-addressed asset store, the signed manifest, and a component
|
|
18
|
+
model where a view is a hash and a handler changes state.
|
|
19
|
+
email:
|
|
20
|
+
- olivier@solisoft.net
|
|
21
|
+
executables: []
|
|
22
|
+
extensions: []
|
|
23
|
+
extra_rdoc_files: []
|
|
24
|
+
files:
|
|
25
|
+
- CHANGELOG.md
|
|
26
|
+
- LICENSE
|
|
27
|
+
- README.md
|
|
28
|
+
- examples/counter.rb
|
|
29
|
+
- lib/eui-ruby.rb
|
|
30
|
+
- lib/eui.rb
|
|
31
|
+
- lib/eui/app.rb
|
|
32
|
+
- lib/eui/assets.rb
|
|
33
|
+
- lib/eui/blake3.rb
|
|
34
|
+
- lib/eui/component.rb
|
|
35
|
+
- lib/eui/dsl.rb
|
|
36
|
+
- lib/eui/errors.rb
|
|
37
|
+
- lib/eui/manifest.rb
|
|
38
|
+
- lib/eui/proto.rb
|
|
39
|
+
- lib/eui/proto/frame.rb
|
|
40
|
+
- lib/eui/proto/limits.rb
|
|
41
|
+
- lib/eui/proto/node.rb
|
|
42
|
+
- lib/eui/proto/op.rb
|
|
43
|
+
- lib/eui/proto/reader.rb
|
|
44
|
+
- lib/eui/proto/style.rb
|
|
45
|
+
- lib/eui/proto/writer.rb
|
|
46
|
+
- lib/eui/server.rb
|
|
47
|
+
- lib/eui/session.rb
|
|
48
|
+
- lib/eui/theme.rb
|
|
49
|
+
- lib/eui/version.rb
|
|
50
|
+
- lib/eui/view/diff.rb
|
|
51
|
+
- lib/eui/view/style.rb
|
|
52
|
+
- lib/eui/view/tree.rb
|
|
53
|
+
- lib/eui/websocket.rb
|
|
54
|
+
homepage: https://github.com/solisoft/eui-ruby
|
|
55
|
+
licenses:
|
|
56
|
+
- MIT
|
|
57
|
+
metadata:
|
|
58
|
+
source_code_uri: https://github.com/solisoft/eui-ruby
|
|
59
|
+
changelog_uri: https://github.com/solisoft/eui-ruby/blob/main/CHANGELOG.md
|
|
60
|
+
documentation_uri: https://github.com/solisoft/eui/tree/main/spec
|
|
61
|
+
rubygems_mfa_required: 'true'
|
|
62
|
+
rdoc_options: []
|
|
63
|
+
require_paths:
|
|
64
|
+
- lib
|
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '3.2'
|
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
requirements: []
|
|
76
|
+
rubygems_version: 3.6.9
|
|
77
|
+
specification_version: 4
|
|
78
|
+
summary: 'EUI applications in Ruby: the wire format, the views, and the server that
|
|
79
|
+
speaks them.'
|
|
80
|
+
test_files: []
|