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.
@@ -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
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The gem is `eui-ruby`; the library is `eui`.
4
+ require_relative 'eui'
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: []