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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/Makefile +134 -0
- data/README.md +247 -0
- data/SINATRA_COMPAT.md +376 -0
- data/bin/tep +2156 -0
- data/examples/agentic_chat/README.md +103 -0
- data/examples/agentic_chat/app.rb +310 -0
- data/examples/api_gateway/README.md +49 -0
- data/examples/api_gateway/app.rb +66 -0
- data/examples/blog/app.rb +367 -0
- data/examples/blog/views/index.erb +36 -0
- data/examples/blog/views/login.erb +28 -0
- data/examples/blog/views/new_post.erb +25 -0
- data/examples/blog/views/show.erb +16 -0
- data/examples/chat/app.rb +278 -0
- data/examples/chat/assets/logo.svg +13 -0
- data/examples/chat/assets/style.css +209 -0
- data/examples/chat/views/index.erb +142 -0
- data/examples/chatbot/README.md +111 -0
- data/examples/chatbot/app.rb +1024 -0
- data/examples/chatbot/assets/chat.js +249 -0
- data/examples/chatbot/assets/compare.js +93 -0
- data/examples/chatbot/assets/markdown.js +84 -0
- data/examples/chatbot/assets/style.css +215 -0
- data/examples/chatbot/schema.sql +25 -0
- data/examples/chatbot/views/compare.erb +43 -0
- data/examples/chatbot/views/index.erb +42 -0
- data/examples/chatbot/views/login.erb +22 -0
- data/examples/chatbot/views/setup.erb +23 -0
- data/examples/counter/README.md +68 -0
- data/examples/counter/app.rb +85 -0
- data/examples/experiments/AGENTS.md +91 -0
- data/examples/experiments/README.md +99 -0
- data/examples/experiments/app.rb +225 -0
- data/examples/geohash/Gemfile +11 -0
- data/examples/geohash/Gemfile.lock +17 -0
- data/examples/geohash/README.md +58 -0
- data/examples/geohash/app.rb +33 -0
- data/examples/hello.rb +120 -0
- data/examples/llm_gateway/README.md +73 -0
- data/examples/llm_gateway/app.rb +91 -0
- data/examples/maidenhead/Gemfile +7 -0
- data/examples/maidenhead/Gemfile.lock +17 -0
- data/examples/maidenhead/README.md +47 -0
- data/examples/maidenhead/app.rb +46 -0
- data/examples/pg_hello.rb +76 -0
- data/examples/qdrant/Gemfile +11 -0
- data/examples/qdrant/Gemfile.lock +29 -0
- data/examples/qdrant/README.md +54 -0
- data/examples/sinatra_style.rb +32 -0
- data/examples/websocket_echo.rb +37 -0
- data/lib/tep/agent_delegation.rb +35 -0
- data/lib/tep/app.rb +291 -0
- data/lib/tep/assets.rb +52 -0
- data/lib/tep/auth.rb +78 -0
- data/lib/tep/auth_bearer_token.rb +126 -0
- data/lib/tep/auth_oauth2.rb +189 -0
- data/lib/tep/auth_oauth2_client.rb +29 -0
- data/lib/tep/auth_oauth2_code.rb +40 -0
- data/lib/tep/auth_session_cookie.rb +132 -0
- data/lib/tep/broadcast.rb +265 -0
- data/lib/tep/broadcast_subscription.rb +42 -0
- data/lib/tep/cache.rb +49 -0
- data/lib/tep/events.rb +257 -0
- data/lib/tep/filter.rb +21 -0
- data/lib/tep/handler.rb +35 -0
- data/lib/tep/http.rb +599 -0
- data/lib/tep/identity.rb +67 -0
- data/lib/tep/job.rb +186 -0
- data/lib/tep/json.rb +572 -0
- data/lib/tep/jwt.rb +126 -0
- data/lib/tep/live_view.rb +219 -0
- data/lib/tep/llm.rb +505 -0
- data/lib/tep/logger.rb +85 -0
- data/lib/tep/mcp.rb +203 -0
- data/lib/tep/multipart.rb +98 -0
- data/lib/tep/net.rb +155 -0
- data/lib/tep/openai_server.rb +725 -0
- data/lib/tep/parallel.rb +168 -0
- data/lib/tep/parser.rb +81 -0
- data/lib/tep/password.rb +102 -0
- data/lib/tep/pg.rb +1128 -0
- data/lib/tep/presence.rb +589 -0
- data/lib/tep/presence_entry.rb +52 -0
- data/lib/tep/proxy.rb +801 -0
- data/lib/tep/request.rb +194 -0
- data/lib/tep/response.rb +134 -0
- data/lib/tep/router.rb +137 -0
- data/lib/tep/scheduler.rb +342 -0
- data/lib/tep/security.rb +140 -0
- data/lib/tep/server.rb +276 -0
- data/lib/tep/server_scheduled.rb +375 -0
- data/lib/tep/session.rb +98 -0
- data/lib/tep/shell.rb +62 -0
- data/lib/tep/sphttp.c +858 -0
- data/lib/tep/sqlite.rb +215 -0
- data/lib/tep/streamer.rb +31 -0
- data/lib/tep/tep_pg.c +769 -0
- data/lib/tep/tep_sqlite.c +320 -0
- data/lib/tep/url.rb +161 -0
- data/lib/tep/version.rb +3 -0
- data/lib/tep/websocket/connection.rb +171 -0
- data/lib/tep/websocket/driver.rb +169 -0
- data/lib/tep/websocket/frame.rb +238 -0
- data/lib/tep/websocket/handshake.rb +159 -0
- data/lib/tep/websocket.rb +68 -0
- data/lib/tep.rb +981 -0
- data/public/hello.txt +1 -0
- data/public/style.css +4 -0
- data/spinel-ext.json +33 -0
- data/test/helper.rb +248 -0
- data/test/real_world/01_simple.rb +5 -0
- data/test/real_world/02_lifecycle.rb +20 -0
- data/test/real_world/03_chat.rb +75 -0
- data/test/real_world/04_health_api.rb +25 -0
- data/test/real_world/05_todo_api.rb +57 -0
- data/test/real_world/06_basic_auth.rb +25 -0
- data/test/real_world/07_bbc_rest_api.rb +228 -0
- data/test/real_world/07_sklise_things.rb +109 -0
- data/test/real_world/08_jwd83_helloworld.rb +56 -0
- data/test/run_all.rb +7 -0
- data/test/run_parallel.rb +89 -0
- data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
- data/test/test_api_gateway.rb +76 -0
- data/test/test_auth.rb +223 -0
- data/test/test_auth_oauth2.rb +208 -0
- data/test/test_auth_session_cookie.rb +198 -0
- data/test/test_broadcast.rb +197 -0
- data/test/test_broadcast_pg.rb +135 -0
- data/test/test_cache.rb +98 -0
- data/test/test_cache_static.rb +48 -0
- data/test/test_cookies.rb +52 -0
- data/test/test_erb.rb +53 -0
- data/test/test_erb_ivars.rb +58 -0
- data/test/test_events.rb +114 -0
- data/test/test_filters.rb +41 -0
- data/test/test_geohash_example.rb +89 -0
- data/test/test_http.rb +137 -0
- data/test/test_http_pool.rb +122 -0
- data/test/test_http_pool_send.rb +57 -0
- data/test/test_identity.rb +165 -0
- data/test/test_inbound_tls.rb +101 -0
- data/test/test_inbound_tls_scheduled.rb +101 -0
- data/test/test_job.rb +108 -0
- data/test/test_json.rb +168 -0
- data/test/test_jwt.rb +143 -0
- data/test/test_live_view.rb +324 -0
- data/test/test_llm.rb +250 -0
- data/test/test_llm_gateway.rb +95 -0
- data/test/test_logger.rb +101 -0
- data/test/test_maidenhead_example.rb +86 -0
- data/test/test_mcp.rb +264 -0
- data/test/test_misc_v02.rb +54 -0
- data/test/test_modular.rb +43 -0
- data/test/test_multi_filters.rb +40 -0
- data/test/test_mustache.rb +57 -0
- data/test/test_openai_server.rb +598 -0
- data/test/test_optional_segments.rb +45 -0
- data/test/test_parallel.rb +102 -0
- data/test/test_params.rb +99 -0
- data/test/test_pass.rb +42 -0
- data/test/test_password.rb +101 -0
- data/test/test_pg.rb +673 -0
- data/test/test_presence.rb +374 -0
- data/test/test_presence_pg.rb +309 -0
- data/test/test_proxy.rb +556 -0
- data/test/test_proxy_dsl.rb +119 -0
- data/test/test_proxy_streaming.rb +146 -0
- data/test/test_real_world.rb +397 -0
- data/test/test_regex_routes.rb +52 -0
- data/test/test_request_methods.rb +102 -0
- data/test/test_response.rb +123 -0
- data/test/test_routing.rb +109 -0
- data/test/test_scheduler.rb +153 -0
- data/test/test_security.rb +72 -0
- data/test/test_server_scheduled.rb +56 -0
- data/test/test_sessions.rb +59 -0
- data/test/test_shell.rb +54 -0
- data/test/test_sqlite.rb +148 -0
- data/test/test_sqlite_cached.rb +171 -0
- data/test/test_static.rb +57 -0
- data/test/test_streaming.rb +96 -0
- data/test/test_unsupported.rb +32 -0
- data/test/test_websocket.rb +152 -0
- data/test/test_websocket_echo.rb +138 -0
- data/test/views/greet.erb +5 -0
- data/test/views/hello.erb +5 -0
- data/test/views/list.erb +5 -0
- data/test/views/m_ivars.mustache +3 -0
- data/test/views/m_simple.mustache +4 -0
- data/test/views/mixed.erb +3 -0
- metadata +264 -0
data/lib/tep/session.rb
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Tep::Session -- string-keyed string store, persisted in a signed
|
|
2
|
+
# cookie. Format: `urlencoded_payload.hexhmac` where the signature
|
|
3
|
+
# covers exactly the urlencoded payload. Forgery-resistant given a
|
|
4
|
+
# strong secret; payload is *visible* to clients (not encrypted).
|
|
5
|
+
#
|
|
6
|
+
# To enable: set `Tep.session_secret` to a long random string at app
|
|
7
|
+
# load time (e.g. `Tep.session_secret = ENV.fetch("TEP_SESSION_SECRET")`).
|
|
8
|
+
# When unset, sessions silently no-op (read-only Bag, no Set-Cookie).
|
|
9
|
+
module Tep
|
|
10
|
+
COOKIE_NAME = "tep.session"
|
|
11
|
+
|
|
12
|
+
class Session
|
|
13
|
+
attr_accessor :data, :dirty
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@data = Tep.str_hash
|
|
17
|
+
@dirty = false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Spinel doesn't dispatch user-defined `[]` / `[]=` on user
|
|
21
|
+
# classes -- and emitting them at all forces those methods to
|
|
22
|
+
# default-typed mrb_int params for callers we don't have, which
|
|
23
|
+
# mismatches the underlying String/String slots. So Session
|
|
24
|
+
# exposes only named methods; the translator rewrites
|
|
25
|
+
# `session[k] = v` to `session.set(k, v)` and `session[k]` to
|
|
26
|
+
# `session.get(k)` for source compatibility with Sinatra.
|
|
27
|
+
def get(k); @data[k]; end
|
|
28
|
+
def set(k, v); @data[k] = v; @dirty = true; end
|
|
29
|
+
def has?(k); @data.key?(k); end
|
|
30
|
+
def length; @data.length; end
|
|
31
|
+
def clear; @data = Tep.str_hash; @dirty = true; end
|
|
32
|
+
|
|
33
|
+
# Verify + decode an inbound cookie value. Returns true on
|
|
34
|
+
# success (data populated), false on missing / tampered.
|
|
35
|
+
def load_from(cookie_value, secret)
|
|
36
|
+
if cookie_value.length == 0 || secret.length == 0
|
|
37
|
+
return false
|
|
38
|
+
end
|
|
39
|
+
dot = cookie_value.rindex(".")
|
|
40
|
+
if dot.nil?
|
|
41
|
+
return false
|
|
42
|
+
end
|
|
43
|
+
payload = cookie_value[0, dot]
|
|
44
|
+
sig = cookie_value[dot + 1, cookie_value.length - dot - 1]
|
|
45
|
+
expect = Crypto.sp_crypto_hmac_sha256_hex(secret, payload)
|
|
46
|
+
if !Tep.timing_safe_eq(sig, expect)
|
|
47
|
+
return false
|
|
48
|
+
end
|
|
49
|
+
Url.parse_query(payload).each do |k, v|
|
|
50
|
+
@data[k] = v
|
|
51
|
+
end
|
|
52
|
+
true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Serialize + sign for the response cookie. Caller decides when
|
|
56
|
+
# to call this (typically only when @dirty).
|
|
57
|
+
def to_cookie_value(secret)
|
|
58
|
+
payload = ""
|
|
59
|
+
first = true
|
|
60
|
+
@data.each do |k, v|
|
|
61
|
+
if !first
|
|
62
|
+
payload = payload + "&"
|
|
63
|
+
end
|
|
64
|
+
payload = payload + Url.escape(k) + "=" + Url.escape(v)
|
|
65
|
+
first = false
|
|
66
|
+
end
|
|
67
|
+
payload + "." + Crypto.sp_crypto_hmac_sha256_hex(secret, payload)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Constant-time string equality. Avoids leaking the matching prefix
|
|
72
|
+
# length via early-exit timing. spinel doesn't have a stdlib
|
|
73
|
+
# crypto-safe compare, so we roll our own.
|
|
74
|
+
def self.timing_safe_eq(a, b)
|
|
75
|
+
if a.length != b.length
|
|
76
|
+
return false
|
|
77
|
+
end
|
|
78
|
+
diff = 0
|
|
79
|
+
i = 0
|
|
80
|
+
n = a.length
|
|
81
|
+
while i < n
|
|
82
|
+
# getbyte(i), NOT bytes[i]: `String#bytes` allocates a fresh
|
|
83
|
+
# array on EVERY iteration (O(n^2) garbage). Besides being slow,
|
|
84
|
+
# that allocation storm drives the GC hard enough to free `b` --
|
|
85
|
+
# the HMAC string returned from the Crypto FFI call, held only in
|
|
86
|
+
# an argument local -- mid-loop, so a valid cookie fails its
|
|
87
|
+
# signature check ~5% of the time under load (a #1052-family
|
|
88
|
+
# heap-local rooting gap in spinel, open on master cc94707; the
|
|
89
|
+
# real fix is upstream, tracked at tep#157). getbyte allocates
|
|
90
|
+
# nothing, removing the dominant GC trigger here (cuts the flake
|
|
91
|
+
# ~3x); the residual lives at other unrooted-local sites in the
|
|
92
|
+
# decode path and clears only when spinel roots heap locals.
|
|
93
|
+
diff = diff | (a.getbyte(i) ^ b.getbyte(i))
|
|
94
|
+
i += 1
|
|
95
|
+
end
|
|
96
|
+
diff == 0
|
|
97
|
+
end
|
|
98
|
+
end
|
data/lib/tep/shell.rb
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Tep::Shell -- minimal popen-based shell-out + /proc-style file
|
|
2
|
+
# reads. The pair covers ~all of what a "system dashboard" needs
|
|
3
|
+
# without dragging in an Open3-equivalent.
|
|
4
|
+
#
|
|
5
|
+
# Security note
|
|
6
|
+
# -------------
|
|
7
|
+
# `run(cmd)` passes its argument verbatim to `/bin/sh -c`. NEVER
|
|
8
|
+
# interpolate untrusted input into the command string -- you'll get
|
|
9
|
+
# a textbook command injection. The same is true of every other
|
|
10
|
+
# popen-style API in any language; we don't pretend otherwise.
|
|
11
|
+
#
|
|
12
|
+
# When you need to feed user-controllable values to a command, build
|
|
13
|
+
# the argv yourself, write to a temp file, or use an explicit allow-
|
|
14
|
+
# list of acceptable inputs.
|
|
15
|
+
module Tep
|
|
16
|
+
class Shell
|
|
17
|
+
DEFAULT_MAX = 65535
|
|
18
|
+
|
|
19
|
+
# Run `cmd` via /bin/sh -c; return up to DEFAULT_MAX bytes of
|
|
20
|
+
# stdout as a string. Stderr is inherited (visible on the
|
|
21
|
+
# server's console / log). The command's exit status is
|
|
22
|
+
# discarded -- callers that need it can append `; echo "EX=$?"`
|
|
23
|
+
# and parse the tail.
|
|
24
|
+
# `+ ""` forces a Ruby-side copy of the static C buffer; see
|
|
25
|
+
# `read` below.
|
|
26
|
+
def self.run(cmd)
|
|
27
|
+
Sock.sphttp_shell_capture(cmd, DEFAULT_MAX) + ""
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# As above but with a caller-chosen byte cap. Lower caps are
|
|
31
|
+
# cheaper memory-wise; higher caps (up to the sphttp internal
|
|
32
|
+
# buffer of ~64KB) let longer outputs through.
|
|
33
|
+
def self.run_limited(cmd, max_bytes)
|
|
34
|
+
Sock.sphttp_shell_capture(cmd, max_bytes) + ""
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Read a file's contents. Useful for /proc/loadavg, /proc/meminfo,
|
|
38
|
+
# /sys/class/thermal/.../temp, and similar small-text endpoints.
|
|
39
|
+
# Returns "" on open failure (spinel's File.read swallows fopen
|
|
40
|
+
# errors and returns the empty string -- matches the prior
|
|
41
|
+
# sphttp_file_read behaviour).
|
|
42
|
+
def self.read(path)
|
|
43
|
+
File.read(path)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Bounded read: slice after the fact. The cap is mostly a
|
|
47
|
+
# defensive cue -- callers that need it should be reading
|
|
48
|
+
# bounded /proc files anyway.
|
|
49
|
+
def self.read_limited(path, max_bytes)
|
|
50
|
+
out = File.read(path)
|
|
51
|
+
out.length > max_bytes ? out[0, max_bytes] : out
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Write `data` to `path` (truncate + rewrite). Returns the byte
|
|
55
|
+
# count for symmetry with the old FFI shape; spinel's File.write
|
|
56
|
+
# is void, so we recover it from data.length.
|
|
57
|
+
def self.write(path, data)
|
|
58
|
+
File.write(path, data)
|
|
59
|
+
data.length
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|