tep 0.11.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.
Files changed (193) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/Makefile +134 -0
  4. data/README.md +247 -0
  5. data/SINATRA_COMPAT.md +376 -0
  6. data/bin/tep +2156 -0
  7. data/examples/agentic_chat/README.md +103 -0
  8. data/examples/agentic_chat/app.rb +310 -0
  9. data/examples/api_gateway/README.md +49 -0
  10. data/examples/api_gateway/app.rb +66 -0
  11. data/examples/blog/app.rb +367 -0
  12. data/examples/blog/views/index.erb +36 -0
  13. data/examples/blog/views/login.erb +28 -0
  14. data/examples/blog/views/new_post.erb +25 -0
  15. data/examples/blog/views/show.erb +16 -0
  16. data/examples/chat/app.rb +278 -0
  17. data/examples/chat/assets/logo.svg +13 -0
  18. data/examples/chat/assets/style.css +209 -0
  19. data/examples/chat/views/index.erb +142 -0
  20. data/examples/chatbot/README.md +111 -0
  21. data/examples/chatbot/app.rb +1024 -0
  22. data/examples/chatbot/assets/chat.js +249 -0
  23. data/examples/chatbot/assets/compare.js +93 -0
  24. data/examples/chatbot/assets/markdown.js +84 -0
  25. data/examples/chatbot/assets/style.css +215 -0
  26. data/examples/chatbot/schema.sql +25 -0
  27. data/examples/chatbot/views/compare.erb +43 -0
  28. data/examples/chatbot/views/index.erb +42 -0
  29. data/examples/chatbot/views/login.erb +22 -0
  30. data/examples/chatbot/views/setup.erb +23 -0
  31. data/examples/counter/README.md +68 -0
  32. data/examples/counter/app.rb +85 -0
  33. data/examples/experiments/AGENTS.md +91 -0
  34. data/examples/experiments/README.md +99 -0
  35. data/examples/experiments/app.rb +225 -0
  36. data/examples/geohash/Gemfile +11 -0
  37. data/examples/geohash/Gemfile.lock +17 -0
  38. data/examples/geohash/README.md +58 -0
  39. data/examples/geohash/app.rb +33 -0
  40. data/examples/hello.rb +120 -0
  41. data/examples/llm_gateway/README.md +73 -0
  42. data/examples/llm_gateway/app.rb +91 -0
  43. data/examples/maidenhead/Gemfile +7 -0
  44. data/examples/maidenhead/Gemfile.lock +17 -0
  45. data/examples/maidenhead/README.md +47 -0
  46. data/examples/maidenhead/app.rb +46 -0
  47. data/examples/pg_hello.rb +76 -0
  48. data/examples/qdrant/Gemfile +11 -0
  49. data/examples/qdrant/Gemfile.lock +29 -0
  50. data/examples/qdrant/README.md +54 -0
  51. data/examples/sinatra_style.rb +32 -0
  52. data/examples/websocket_echo.rb +37 -0
  53. data/lib/tep/agent_delegation.rb +35 -0
  54. data/lib/tep/app.rb +291 -0
  55. data/lib/tep/assets.rb +52 -0
  56. data/lib/tep/auth.rb +78 -0
  57. data/lib/tep/auth_bearer_token.rb +126 -0
  58. data/lib/tep/auth_oauth2.rb +189 -0
  59. data/lib/tep/auth_oauth2_client.rb +29 -0
  60. data/lib/tep/auth_oauth2_code.rb +40 -0
  61. data/lib/tep/auth_session_cookie.rb +132 -0
  62. data/lib/tep/broadcast.rb +265 -0
  63. data/lib/tep/broadcast_subscription.rb +42 -0
  64. data/lib/tep/cache.rb +49 -0
  65. data/lib/tep/events.rb +257 -0
  66. data/lib/tep/filter.rb +21 -0
  67. data/lib/tep/handler.rb +35 -0
  68. data/lib/tep/http.rb +599 -0
  69. data/lib/tep/identity.rb +67 -0
  70. data/lib/tep/job.rb +186 -0
  71. data/lib/tep/json.rb +572 -0
  72. data/lib/tep/jwt.rb +126 -0
  73. data/lib/tep/live_view.rb +219 -0
  74. data/lib/tep/llm.rb +505 -0
  75. data/lib/tep/logger.rb +85 -0
  76. data/lib/tep/mcp.rb +203 -0
  77. data/lib/tep/multipart.rb +98 -0
  78. data/lib/tep/net.rb +155 -0
  79. data/lib/tep/openai_server.rb +725 -0
  80. data/lib/tep/parallel.rb +168 -0
  81. data/lib/tep/parser.rb +81 -0
  82. data/lib/tep/password.rb +102 -0
  83. data/lib/tep/pg.rb +1128 -0
  84. data/lib/tep/presence.rb +589 -0
  85. data/lib/tep/presence_entry.rb +52 -0
  86. data/lib/tep/proxy.rb +801 -0
  87. data/lib/tep/request.rb +194 -0
  88. data/lib/tep/response.rb +134 -0
  89. data/lib/tep/router.rb +137 -0
  90. data/lib/tep/scheduler.rb +342 -0
  91. data/lib/tep/security.rb +140 -0
  92. data/lib/tep/server.rb +276 -0
  93. data/lib/tep/server_scheduled.rb +375 -0
  94. data/lib/tep/session.rb +98 -0
  95. data/lib/tep/shell.rb +62 -0
  96. data/lib/tep/sphttp.c +858 -0
  97. data/lib/tep/sqlite.rb +215 -0
  98. data/lib/tep/streamer.rb +31 -0
  99. data/lib/tep/tep_pg.c +769 -0
  100. data/lib/tep/tep_sqlite.c +320 -0
  101. data/lib/tep/url.rb +161 -0
  102. data/lib/tep/version.rb +3 -0
  103. data/lib/tep/websocket/connection.rb +171 -0
  104. data/lib/tep/websocket/driver.rb +169 -0
  105. data/lib/tep/websocket/frame.rb +238 -0
  106. data/lib/tep/websocket/handshake.rb +159 -0
  107. data/lib/tep/websocket.rb +68 -0
  108. data/lib/tep.rb +981 -0
  109. data/public/hello.txt +1 -0
  110. data/public/style.css +4 -0
  111. data/spinel-ext.json +33 -0
  112. data/test/helper.rb +248 -0
  113. data/test/real_world/01_simple.rb +5 -0
  114. data/test/real_world/02_lifecycle.rb +20 -0
  115. data/test/real_world/03_chat.rb +75 -0
  116. data/test/real_world/04_health_api.rb +25 -0
  117. data/test/real_world/05_todo_api.rb +57 -0
  118. data/test/real_world/06_basic_auth.rb +25 -0
  119. data/test/real_world/07_bbc_rest_api.rb +228 -0
  120. data/test/real_world/07_sklise_things.rb +109 -0
  121. data/test/real_world/08_jwd83_helloworld.rb +56 -0
  122. data/test/run_all.rb +7 -0
  123. data/test/run_parallel.rb +89 -0
  124. data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
  125. data/test/test_api_gateway.rb +76 -0
  126. data/test/test_auth.rb +223 -0
  127. data/test/test_auth_oauth2.rb +208 -0
  128. data/test/test_auth_session_cookie.rb +198 -0
  129. data/test/test_broadcast.rb +197 -0
  130. data/test/test_broadcast_pg.rb +135 -0
  131. data/test/test_cache.rb +98 -0
  132. data/test/test_cache_static.rb +48 -0
  133. data/test/test_cookies.rb +52 -0
  134. data/test/test_erb.rb +53 -0
  135. data/test/test_erb_ivars.rb +58 -0
  136. data/test/test_events.rb +114 -0
  137. data/test/test_filters.rb +41 -0
  138. data/test/test_geohash_example.rb +89 -0
  139. data/test/test_http.rb +137 -0
  140. data/test/test_http_pool.rb +122 -0
  141. data/test/test_http_pool_send.rb +57 -0
  142. data/test/test_identity.rb +165 -0
  143. data/test/test_inbound_tls.rb +101 -0
  144. data/test/test_inbound_tls_scheduled.rb +101 -0
  145. data/test/test_job.rb +108 -0
  146. data/test/test_json.rb +168 -0
  147. data/test/test_jwt.rb +143 -0
  148. data/test/test_live_view.rb +324 -0
  149. data/test/test_llm.rb +250 -0
  150. data/test/test_llm_gateway.rb +95 -0
  151. data/test/test_logger.rb +101 -0
  152. data/test/test_maidenhead_example.rb +86 -0
  153. data/test/test_mcp.rb +264 -0
  154. data/test/test_misc_v02.rb +54 -0
  155. data/test/test_modular.rb +43 -0
  156. data/test/test_multi_filters.rb +40 -0
  157. data/test/test_mustache.rb +57 -0
  158. data/test/test_openai_server.rb +598 -0
  159. data/test/test_optional_segments.rb +45 -0
  160. data/test/test_parallel.rb +102 -0
  161. data/test/test_params.rb +99 -0
  162. data/test/test_pass.rb +42 -0
  163. data/test/test_password.rb +101 -0
  164. data/test/test_pg.rb +673 -0
  165. data/test/test_presence.rb +374 -0
  166. data/test/test_presence_pg.rb +309 -0
  167. data/test/test_proxy.rb +556 -0
  168. data/test/test_proxy_dsl.rb +119 -0
  169. data/test/test_proxy_streaming.rb +146 -0
  170. data/test/test_real_world.rb +397 -0
  171. data/test/test_regex_routes.rb +52 -0
  172. data/test/test_request_methods.rb +102 -0
  173. data/test/test_response.rb +123 -0
  174. data/test/test_routing.rb +109 -0
  175. data/test/test_scheduler.rb +153 -0
  176. data/test/test_security.rb +72 -0
  177. data/test/test_server_scheduled.rb +56 -0
  178. data/test/test_sessions.rb +59 -0
  179. data/test/test_shell.rb +54 -0
  180. data/test/test_sqlite.rb +148 -0
  181. data/test/test_sqlite_cached.rb +171 -0
  182. data/test/test_static.rb +57 -0
  183. data/test/test_streaming.rb +96 -0
  184. data/test/test_unsupported.rb +32 -0
  185. data/test/test_websocket.rb +152 -0
  186. data/test/test_websocket_echo.rb +138 -0
  187. data/test/views/greet.erb +5 -0
  188. data/test/views/hello.erb +5 -0
  189. data/test/views/list.erb +5 -0
  190. data/test/views/m_ivars.mustache +3 -0
  191. data/test/views/m_simple.mustache +4 -0
  192. data/test/views/mixed.erb +3 -0
  193. metadata +264 -0
@@ -0,0 +1,169 @@
1
+ # Tep::WebSocket::Driver -- Faye-shape state machine + event dispatch.
2
+ #
3
+ # Constructed AFTER the handshake completes, before the recv loop
4
+ # starts. Holds per-connection state + outbound write methods +
5
+ # the event-callback registry that the handler (or the Phase 3 DSL)
6
+ # populates.
7
+ #
8
+ # Faye-shape API (matches faye/websocket-driver-ruby's surface for
9
+ # the parts tep ships -- single-frame text/binary, ping/pong, close):
10
+ #
11
+ # drv = Tep::WebSocket::Driver.new(fd)
12
+ # drv.on_message do |evt| ... end # block-based on:open/on:message etc
13
+ # drv.on_close do |evt| ... end # are syntactic sugar; tep ships
14
+ # drv.text("hi") # the explicit setters instead
15
+ # drv.binary(bytes)
16
+ # drv.ping("")
17
+ # drv.close(1000, "bye")
18
+ #
19
+ # In Phase 2, callbacks are set via explicit setters (`set_on_message`)
20
+ # rather than `on(:message) { block }` since spinel's block-with-
21
+ # closure-on-locals support is still uneven outside Fiber.new bodies.
22
+ # Phase 3's DSL hides this behind `ws.on(:message) { ... }` once we
23
+ # decide on the lowering shape.
24
+ module Tep
25
+ module WebSocket
26
+ class Driver
27
+ attr_accessor :fd, :max_frame_size, :subprotocol
28
+ # Callback slots. Each holds a subclass of Tep::WebSocket::Handler
29
+ # (or the base) that gets `handle_event(event)` called when the
30
+ # corresponding wire event arrives. Defaults to a no-op base
31
+ # so the slot is type-safe pre-set.
32
+ attr_accessor :h_open, :h_message, :h_close, :h_ping, :h_pong, :h_error
33
+
34
+ def initialize(fd)
35
+ @fd = fd
36
+ @max_frame_size = Tep::WebSocket::DEFAULT_MAX_FRAME
37
+ @subprotocol = ""
38
+ @h_open = Tep::WebSocket::Handler.new
39
+ @h_message = Tep::WebSocket::Handler.new
40
+ @h_close = Tep::WebSocket::Handler.new
41
+ @h_ping = Tep::WebSocket::Handler.new
42
+ @h_pong = Tep::WebSocket::Handler.new
43
+ @h_error = Tep::WebSocket::Handler.new
44
+ end
45
+
46
+ def set_max_frame_size(n)
47
+ @max_frame_size = n
48
+ end
49
+
50
+ # Reassign the underlying fd. Used by the server-side upgrade
51
+ # path: the user handler builds the Driver with a placeholder
52
+ # fd (since the client fd isn't visible at handler-dispatch
53
+ # time), and the write_response branch sets the real fd here
54
+ # right before constructing the Connection.
55
+ def set_fd(new_fd)
56
+ @fd = new_fd
57
+ end
58
+
59
+ def set_subprotocol(name)
60
+ @subprotocol = name
61
+ end
62
+
63
+ def set_on_open(h); @h_open = h; end
64
+ def set_on_message(h); @h_message = h; end
65
+ def set_on_close(h); @h_close = h; end
66
+ def set_on_ping(h); @h_ping = h; end
67
+ def set_on_pong(h); @h_pong = h; end
68
+ def set_on_error(h); @h_error = h; end
69
+
70
+ # Send a text frame.
71
+ def text(s)
72
+ Driver.send_frame(@fd, Tep::WebSocket::OPCODE_TEXT, s)
73
+ end
74
+
75
+ # Streamer-shape alias for `text` so a Driver can stand in
76
+ # anywhere `Tep::Streamer`-style code calls `out.write(s)`.
77
+ # Used by Tep::Llm.chat_stream to write LLM deltas as WS
78
+ # frames (one frame per SSE-shaped chunk).
79
+ def write(s)
80
+ Driver.send_frame(@fd, Tep::WebSocket::OPCODE_TEXT, s)
81
+ end
82
+
83
+ # Send a binary frame.
84
+ def binary(bytes)
85
+ Driver.send_frame(@fd, Tep::WebSocket::OPCODE_BINARY, bytes)
86
+ end
87
+
88
+ # Send a ping with optional payload (<=125 bytes).
89
+ def ping(payload)
90
+ Driver.send_frame(@fd, Tep::WebSocket::OPCODE_PING, payload)
91
+ end
92
+
93
+ # Send a pong with the matching ping's payload (per §5.5.3).
94
+ def pong(payload)
95
+ Driver.send_frame(@fd, Tep::WebSocket::OPCODE_PONG, payload)
96
+ end
97
+
98
+ # Send a close frame with code + reason. Reason capped at
99
+ # 123 bytes so the 2-byte code + reason fits in a control
100
+ # frame's 125-byte payload limit.
101
+ def close(code, reason)
102
+ body = Driver.encode_close_payload(code, reason)
103
+ Driver.send_frame(@fd, Tep::WebSocket::OPCODE_CLOSE, body)
104
+ end
105
+
106
+ # Build the frame bytes (unmasked, server-side) and write via
107
+ # sphttp_write_bytes (binary-safe, explicit length).
108
+ def self.send_frame(fd, opcode, payload)
109
+ frame = Tep::WebSocket::Frame.new(true, opcode, payload)
110
+ bytes = frame.encode_unmasked
111
+ Sock.sphttp_write_bytes(fd, bytes, bytes.length)
112
+ end
113
+
114
+ # Close payload: 2-byte big-endian code + UTF-8 reason. Per
115
+ # §5.5.1 the payload may be omitted (close with no body); if
116
+ # `code == 0` we emit an empty payload.
117
+ def self.encode_close_payload(code, reason)
118
+ if code == 0
119
+ return ""
120
+ end
121
+ out = Tep::WebSocket::Frame.byte_to_chr((code >> 8) & 0xff) +
122
+ Tep::WebSocket::Frame.byte_to_chr(code & 0xff)
123
+ if reason.length > 123
124
+ out + reason[0, 123]
125
+ else
126
+ out + reason
127
+ end
128
+ end
129
+ end
130
+
131
+ # Event passed to handler callbacks. Holds `data` (the payload
132
+ # as String for text/binary, raw bytes for ping/pong, or the
133
+ # close code+reason for close) and a numeric `code` for close.
134
+ class Event
135
+ attr_accessor :data, :code, :reason
136
+
137
+ def initialize
138
+ @data = ""
139
+ @code = 0
140
+ @reason = ""
141
+ end
142
+ end
143
+
144
+ # Base class for event handlers. Subclass + override
145
+ # `handle_event(event)`. The Driver stores one Handler instance
146
+ # per event type and dispatches via `@h_message.handle_event(evt)`.
147
+ # The explicit-Handler shape (vs faye's block-based `driver.on(:msg)
148
+ # { ... }`) is chosen because it stays compatible with future
149
+ # Fiber.storage per-connection state plumbing without re-typing
150
+ # the callback boundary.
151
+ #
152
+ # `req` is set at WS upgrade time by the route handler the
153
+ # translator emits, giving on_X handler bodies access to the
154
+ # request that initiated the connection (req.identity,
155
+ # req.session, headers, ...). It stays the same across every
156
+ # event on the connection -- there's no per-frame "request".
157
+ class Handler
158
+ attr_accessor :req
159
+
160
+ def initialize
161
+ @req = Tep::Request.new
162
+ end
163
+
164
+ def handle_event(event)
165
+ 0
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,238 @@
1
+ # Tep::WebSocket::Frame -- single-frame codec.
2
+ #
3
+ # Surface:
4
+ # - Frame.new(fin, opcode, payload) build for emit
5
+ # - frame.encode_unmasked -> String server-side emit bytes
6
+ # - Frame.parse_from_buf(bytes_at, bytes_len) parse a recv'd frame
7
+ # returns a ParseResult (frame + bytes_consumed, OR an error code).
8
+ #
9
+ # Server-side emit: never masks (RFC 6455 §5.3 -- server MUST NOT
10
+ # mask). Client-side emit isn't shipped here; tep is server-shaped.
11
+ #
12
+ # Parse handles three length encodings (7-bit / 16-bit / 64-bit),
13
+ # the 4-byte mask key, and applies the mask to recover the plaintext
14
+ # payload. Returns a structural error code (close-code-shaped) for
15
+ # the family of malformed-frame cases that warrant a 1002 close:
16
+ # - reserved bits set
17
+ # - reserved opcode
18
+ # - client frame not masked
19
+ # - control frame payload > 125
20
+ # - control frame fragmented
21
+ module Tep
22
+ module WebSocket
23
+ class Frame
24
+ attr_accessor :fin, :opcode, :payload
25
+
26
+ def initialize(fin, opcode, payload)
27
+ @fin = fin
28
+ @opcode = opcode
29
+ @payload = payload
30
+ end
31
+
32
+ # Build the unmasked server-side wire bytes. Length-encoding
33
+ # picks the smallest form that fits the payload. No mask.
34
+ def encode_unmasked
35
+ head = ""
36
+ b0 = (@fin ? 0x80 : 0x00) | (@opcode & 0x0f)
37
+ head = head + Frame.byte_to_chr(b0)
38
+
39
+ plen = @payload.length
40
+ if plen <= 125
41
+ head = head + Frame.byte_to_chr(plen)
42
+ elsif plen <= 65535
43
+ head = head + Frame.byte_to_chr(126)
44
+ head = head + Frame.byte_to_chr((plen >> 8) & 0xff)
45
+ head = head + Frame.byte_to_chr(plen & 0xff)
46
+ else
47
+ head = head + Frame.byte_to_chr(127)
48
+ i = 7
49
+ while i >= 0
50
+ head = head + Frame.byte_to_chr((plen >> (i * 8)) & 0xff)
51
+ i -= 1
52
+ end
53
+ end
54
+ head + @payload
55
+ end
56
+
57
+ # Convert a single byte value (0..255) to a 1-char String.
58
+ def self.byte_to_chr(n)
59
+ (n & 0xff).chr
60
+ end
61
+
62
+ # Parse one frame from the sphttp recv frame buffer. `start`
63
+ # is the byte offset to begin reading; `avail` is the count of
64
+ # valid bytes in the buffer. Byte reads go through the Ruby
65
+ # String binding sphttp_recv_frame_buf returns; matz/spinel#657
66
+ # made slice / bytes[i] survive embedded NULs so binary
67
+ # payloads parse correctly without the per-byte C accessor we
68
+ # used before.
69
+ #
70
+ # Returns a ParseResult with one of three shapes:
71
+ # .status == "ok" -> .frame populated + .consumed bytes used
72
+ # .status == "need" -> need more bytes (consumed == 0)
73
+ # .status == "close" -> protocol violation; close with .close_code
74
+ def self.parse_from_buf(start, avail)
75
+ out = Tep::WebSocket::ParseResult.new
76
+ if avail - start < 2
77
+ out.outcome = "need"
78
+ return out
79
+ end
80
+
81
+ buf = Sock.sphttp_recv_frame_buf
82
+ bs = buf.bytes
83
+ b0 = bs[start]
84
+ b1 = bs[start + 1]
85
+ fin = (b0 & 0x80) != 0
86
+ rsv = b0 & 0x70
87
+ opcode = b0 & 0x0f
88
+ masked = (b1 & 0x80) != 0
89
+ len7 = b1 & 0x7f
90
+
91
+ if rsv != 0
92
+ out.outcome = "close"
93
+ out.close_code = Tep::WebSocket::CLOSE_PROTOCOL_ERROR
94
+ return out
95
+ end
96
+ if Frame.reserved_opcode?(opcode)
97
+ out.outcome = "close"
98
+ out.close_code = Tep::WebSocket::CLOSE_PROTOCOL_ERROR
99
+ return out
100
+ end
101
+ if Frame.control_opcode?(opcode)
102
+ if !fin
103
+ out.outcome = "close"
104
+ out.close_code = Tep::WebSocket::CLOSE_PROTOCOL_ERROR
105
+ return out
106
+ end
107
+ if len7 > 125
108
+ out.outcome = "close"
109
+ out.close_code = Tep::WebSocket::CLOSE_PROTOCOL_ERROR
110
+ return out
111
+ end
112
+ end
113
+ if !masked
114
+ # Server MUST close on unmasked client frame (§5.3).
115
+ out.outcome = "close"
116
+ out.close_code = Tep::WebSocket::CLOSE_PROTOCOL_ERROR
117
+ return out
118
+ end
119
+
120
+ # Decode payload length.
121
+ pos = start + 2
122
+ plen = 0
123
+ if len7 < 126
124
+ plen = len7
125
+ elsif len7 == 126
126
+ if avail - pos < 2
127
+ out.outcome = "need"
128
+ return out
129
+ end
130
+ h = bs[pos]
131
+ l = bs[pos + 1]
132
+ plen = (h << 8) | l
133
+ pos += 2
134
+ else
135
+ # 64-bit length
136
+ if avail - pos < 8
137
+ out.outcome = "need"
138
+ return out
139
+ end
140
+ plen = 0
141
+ i = 0
142
+ while i < 8
143
+ plen = (plen << 8) | bs[pos + i]
144
+ i += 1
145
+ end
146
+ pos += 8
147
+ end
148
+
149
+ # 4-byte mask key.
150
+ if avail - pos < 4
151
+ out.outcome = "need"
152
+ return out
153
+ end
154
+ m0 = bs[pos]
155
+ m1 = bs[pos + 1]
156
+ m2 = bs[pos + 2]
157
+ m3 = bs[pos + 3]
158
+ pos += 4
159
+
160
+ # Payload bytes.
161
+ if avail - pos < plen
162
+ out.outcome = "need"
163
+ return out
164
+ end
165
+
166
+ # Decode + unmask in one pass.
167
+ payload = ""
168
+ i = 0
169
+ while i < plen
170
+ b = bs[pos + i]
171
+ mask_byte = 0
172
+ if (i & 3) == 0
173
+ mask_byte = m0
174
+ elsif (i & 3) == 1
175
+ mask_byte = m1
176
+ elsif (i & 3) == 2
177
+ mask_byte = m2
178
+ else
179
+ mask_byte = m3
180
+ end
181
+ payload = payload + Frame.byte_to_chr(b ^ mask_byte)
182
+ i += 1
183
+ end
184
+
185
+ out.outcome = "ok"
186
+ out.frame = Tep::WebSocket::Frame.new(fin, opcode, payload)
187
+ out.consumed = pos + plen - start
188
+ out
189
+ end
190
+
191
+ def self.reserved_opcode?(op)
192
+ if op == Tep::WebSocket::OPCODE_CONTINUATION
193
+ return false
194
+ end
195
+ if op == Tep::WebSocket::OPCODE_TEXT
196
+ return false
197
+ end
198
+ if op == Tep::WebSocket::OPCODE_BINARY
199
+ return false
200
+ end
201
+ if op == Tep::WebSocket::OPCODE_CLOSE
202
+ return false
203
+ end
204
+ if op == Tep::WebSocket::OPCODE_PING
205
+ return false
206
+ end
207
+ if op == Tep::WebSocket::OPCODE_PONG
208
+ return false
209
+ end
210
+ true
211
+ end
212
+
213
+ def self.control_opcode?(op)
214
+ op == Tep::WebSocket::OPCODE_CLOSE ||
215
+ op == Tep::WebSocket::OPCODE_PING ||
216
+ op == Tep::WebSocket::OPCODE_PONG
217
+ end
218
+ end
219
+
220
+ # ParseResult carries either a parsed frame, a "need more
221
+ # bytes" signal, or a close-code for a protocol violation.
222
+ # Field is named `outcome` (not `status`) because attr_accessor
223
+ # :status collides with Tep::Response.status (Integer) under
224
+ # spinel's same-name-attr unification family
225
+ # (matz/spinel#537 / #538), widening Tep.reason(status) to
226
+ # accept poly and breaking the build.
227
+ class ParseResult
228
+ attr_accessor :outcome, :frame, :consumed, :close_code
229
+
230
+ def initialize
231
+ @outcome = ""
232
+ @frame = Tep::WebSocket::Frame.new(true, 0, "")
233
+ @consumed = 0
234
+ @close_code = 0
235
+ end
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,159 @@
1
+ # Tep::WebSocket::Handshake -- RFC 6455 §1.3 server-side handshake.
2
+ #
3
+ # `check(req)`:
4
+ # Returns a Result with `.valid` true if the request is a proper
5
+ # WebSocket upgrade, `.accept_key` set to the Sec-WebSocket-Accept
6
+ # value the server should echo, and `.protocols` parsed from
7
+ # Sec-WebSocket-Protocol. Invalid uses set `.valid = false` +
8
+ # `.reason` for logging.
9
+ #
10
+ # `build_response(accept_key, protocol)`:
11
+ # Returns the raw HTTP/1.1 101 Switching Protocols response bytes,
12
+ # ready to write to the socket. `protocol` is the subprotocol to
13
+ # echo (empty string = omit the header per RFC §1.3 -- the safe
14
+ # default per rubys's pushback on tep#8).
15
+ module Tep
16
+ module WebSocket
17
+ class Handshake
18
+ def self.check(req)
19
+ out = Tep::WebSocket::Handshake::Result.new
20
+
21
+ # Verb must be GET.
22
+ if req.verb != "GET"
23
+ out.valid = false
24
+ out.reason = "bad verb"
25
+ return out
26
+ end
27
+
28
+ # Upgrade + Connection headers (downcased per Tep::Request).
29
+ upgrade = req.headers["upgrade"]
30
+ if Handshake.icontains(upgrade, "websocket") == false
31
+ out.valid = false
32
+ out.reason = "missing/invalid Upgrade"
33
+ return out
34
+ end
35
+ conn = req.headers["connection"]
36
+ if Handshake.icontains(conn, "upgrade") == false
37
+ out.valid = false
38
+ out.reason = "missing/invalid Connection"
39
+ return out
40
+ end
41
+
42
+ # Sec-WebSocket-Version must be 13.
43
+ ver = req.headers["sec-websocket-version"]
44
+ if ver != "13"
45
+ out.valid = false
46
+ out.reason = "bad/missing Sec-WebSocket-Version"
47
+ return out
48
+ end
49
+
50
+ # Sec-WebSocket-Key: 24-char base64 (16-byte nonce).
51
+ key = req.headers["sec-websocket-key"]
52
+ if key.length == 0
53
+ out.valid = false
54
+ out.reason = "missing Sec-WebSocket-Key"
55
+ return out
56
+ end
57
+
58
+ out.valid = true
59
+ out.accept_key = Crypto.sp_crypto_websocket_accept(key)
60
+
61
+ # Parse Sec-WebSocket-Protocol (comma-separated). Handler
62
+ # gets the offered list; can opt-in via Driver.accept_protocol.
63
+ out.protocols = Handshake.split_csv(req.headers["sec-websocket-protocol"])
64
+ out
65
+ end
66
+
67
+ # Build the 101 Switching Protocols response. `protocol` empty
68
+ # = omit Sec-WebSocket-Protocol entirely (spec-correct per
69
+ # RFC 6455 §4.2.2; better than echoing a protocol the server
70
+ # doesn't actually implement).
71
+ def self.build_response(accept_key, protocol)
72
+ out = "HTTP/1.1 101 Switching Protocols\r\n" +
73
+ "Upgrade: websocket\r\n" +
74
+ "Connection: Upgrade\r\n" +
75
+ "Sec-WebSocket-Accept: " + accept_key + "\r\n"
76
+ if protocol.length > 0
77
+ out = out + "Sec-WebSocket-Protocol: " + protocol + "\r\n"
78
+ end
79
+ out + "\r\n"
80
+ end
81
+
82
+ # Case-insensitive substring contains. Hand-rolled because
83
+ # Tep::Request normalises header names to lowercase but
84
+ # leaves values as-is, and clients sometimes send
85
+ # `Connection: keep-alive, Upgrade` capitalised.
86
+ def self.icontains(hay, needle)
87
+ if hay.length == 0 || needle.length == 0
88
+ return false
89
+ end
90
+ hl = Handshake.downcase(hay)
91
+ nl = Handshake.downcase(needle)
92
+ Tep.str_find(hl, nl, 0) >= 0
93
+ end
94
+
95
+ def self.downcase(s)
96
+ out = ""
97
+ i = 0
98
+ while i < s.length
99
+ c = s[i]
100
+ if c >= "A" && c <= "Z"
101
+ out = out + (c.ord + 32).chr
102
+ else
103
+ out = out + c
104
+ end
105
+ i += 1
106
+ end
107
+ out
108
+ end
109
+
110
+ # Parse comma-separated header value into an Array<String>.
111
+ # Trims whitespace around each entry.
112
+ def self.split_csv(s)
113
+ out = [""]
114
+ out.delete_at(0)
115
+ if s.length == 0
116
+ return out
117
+ end
118
+ pos = 0
119
+ while pos < s.length
120
+ comma = Tep.str_find(s, ",", pos)
121
+ if comma < 0
122
+ out.push(Handshake.trim(s[pos, s.length - pos]))
123
+ return out
124
+ end
125
+ out.push(Handshake.trim(s[pos, comma - pos]))
126
+ pos = comma + 1
127
+ end
128
+ out
129
+ end
130
+
131
+ def self.trim(s)
132
+ i = 0
133
+ while i < s.length && (s[i] == " " || s[i] == "\t")
134
+ i += 1
135
+ end
136
+ j = s.length - 1
137
+ while j >= i && (s[j] == " " || s[j] == "\t")
138
+ j -= 1
139
+ end
140
+ if j < i
141
+ return ""
142
+ end
143
+ s[i, j - i + 1]
144
+ end
145
+
146
+ class Result
147
+ attr_accessor :valid, :reason, :accept_key, :protocols
148
+
149
+ def initialize
150
+ @valid = false
151
+ @reason = ""
152
+ @accept_key = ""
153
+ @protocols = [""]
154
+ @protocols.delete_at(0)
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,68 @@
1
+ # Tep::WebSocket -- RFC 6455 WebSocket support for spinel-AOT'd apps.
2
+ #
3
+ # Protocol substrate (this file's directory):
4
+ # - Tep::WebSocket::Frame single-frame codec (parse + emit)
5
+ # - Tep::WebSocket::Handshake server-side handshake check + response
6
+ # - Tep::WebSocket::Driver state machine + event dispatch + writers
7
+ # - Tep::WebSocket::Connection fiber-driven recv loop (one fiber per conn)
8
+ #
9
+ # Sinatra-style DSL (lowered by bin/tep):
10
+ #
11
+ # set :scheduler, :scheduled
12
+ # websocket '/chat' do |ws|
13
+ # on_open { |evt| ws.text("welcome") }
14
+ # on_message { |evt| ws.text("echo: " + evt.data) }
15
+ # on_close { |evt| ... }
16
+ # end
17
+ #
18
+ # Requires the scheduled server (the recv loop parks on
19
+ # Tep::Scheduler.io_wait); the blocking server returns 501 on a WS
20
+ # upgrade attempt. See examples/websocket_echo.rb for a full app and
21
+ # test/test_websocket_echo.rb for the end-to-end smoke harness.
22
+ #
23
+ # Compliance posture (per the OriPekelman/tep#8 strict/lenient table):
24
+ # strict-emit: server NEVER masks; reserved bits 0 on emit
25
+ # strict-accept (close 1002):
26
+ # - client frames MUST be masked
27
+ # - reserved bits RSV1-3 MUST be 0
28
+ # - reserved opcodes (3-7, B-F) reject
29
+ # - control frame payload > 125 reject
30
+ # - control frames MUST NOT fragment
31
+ # - continuation without prior fragment reject
32
+ # strict-accept (close 1007): text frames MUST be UTF-8 (deferred to
33
+ # Phase 2.1 -- the codec ships the structural strictness first;
34
+ # the UTF-8 validator is its own ~50 LOC).
35
+ # liberal-accept: close codes, pong payload contents, unsolicited pong.
36
+ module Tep
37
+ module WebSocket
38
+ # Standard opcodes.
39
+ OPCODE_CONTINUATION = 0
40
+ OPCODE_TEXT = 1
41
+ OPCODE_BINARY = 2
42
+ OPCODE_CLOSE = 8
43
+ OPCODE_PING = 9
44
+ OPCODE_PONG = 10
45
+
46
+ # Close codes (RFC 6455 §7.4). Caller-facing ones only -- the
47
+ # internal-error / protocol-error codes are emitted by the
48
+ # Driver directly, not exposed.
49
+ CLOSE_NORMAL = 1000
50
+ CLOSE_GOING_AWAY = 1001
51
+ CLOSE_PROTOCOL_ERROR = 1002
52
+ CLOSE_UNSUPPORTED = 1003
53
+ CLOSE_INVALID_UTF8 = 1007
54
+ CLOSE_POLICY_VIOLATION = 1008
55
+ CLOSE_MESSAGE_TOO_BIG = 1009
56
+
57
+ # Frame-size cap. Configurable via Driver#set_max_frame_size;
58
+ # default is 16 MiB (large enough for any realistic chat /
59
+ # Action Cable payload, bounded so an oversized frame can be
60
+ # closed with 1009 rather than OOM-ing the worker).
61
+ DEFAULT_MAX_FRAME = 16 * 1024 * 1024
62
+ end
63
+ end
64
+
65
+ require_relative "websocket/frame"
66
+ require_relative "websocket/handshake"
67
+ require_relative "websocket/driver"
68
+ require_relative "websocket/connection"