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/mcp.rb
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Tep::MCP -- runtime helpers for the MCP battery (chunk 5.1).
|
|
2
|
+
#
|
|
3
|
+
# Most of the action happens in the bin/tep translator: each
|
|
4
|
+
# `mcp_tool` declaration generates a per-tool dispatch cmeth + a
|
|
5
|
+
# direct HTTP route, and the translator-emitted dispatcher class
|
|
6
|
+
# at POST /mcp routes JSON-RPC 2.0 messages to those cmeths by
|
|
7
|
+
# name. This file holds the runtime helpers the generated code
|
|
8
|
+
# leans on -- nested-key JSON extraction, result builders, and
|
|
9
|
+
# JSON-RPC envelope formatters.
|
|
10
|
+
#
|
|
11
|
+
# Public surface (chunk 5.1):
|
|
12
|
+
#
|
|
13
|
+
# Tep::MCP.text(s) -> Result with text content
|
|
14
|
+
# Tep::MCP.error(s) -> Result marked isError = true
|
|
15
|
+
# Tep::MCP.nested_extract(j, k) -> sub-JSON string for a nested key
|
|
16
|
+
# Tep::MCP.initialize_envelope(id, name, version)
|
|
17
|
+
# Tep::MCP.tools_list_envelope(id, tools_json)
|
|
18
|
+
# Tep::MCP.tools_call_envelope(id, result)
|
|
19
|
+
# Tep::MCP.unknown_tool_envelope(id, name)
|
|
20
|
+
# Tep::MCP.method_not_found_envelope(id, method)
|
|
21
|
+
#
|
|
22
|
+
# Apps wire the battery via `mcp_tool '...' do ... end` blocks at
|
|
23
|
+
# the top level; bin/tep does the rest. The runtime here stays
|
|
24
|
+
# small + spinel-friendly (no class-hierarchy dispatch, no
|
|
25
|
+
# heterogeneous arrays). See docs/MCP-BATTERY.md for the design.
|
|
26
|
+
module Tep
|
|
27
|
+
module MCP
|
|
28
|
+
# MCP protocol version this server claims to speak. Tracks the
|
|
29
|
+
# 2025-03 ("Streamable HTTP") revision of the spec.
|
|
30
|
+
PROTOCOL_VERSION = "2025-03-26"
|
|
31
|
+
|
|
32
|
+
# Tool result -- carries either a text content block (the only
|
|
33
|
+
# content type supported in chunk 5.1) or an error marker.
|
|
34
|
+
class Result
|
|
35
|
+
attr_accessor :text, :is_error
|
|
36
|
+
|
|
37
|
+
def initialize
|
|
38
|
+
@text = ""
|
|
39
|
+
@is_error = 0
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.text(s)
|
|
44
|
+
r = Tep::MCP::Result.new
|
|
45
|
+
r.text = s
|
|
46
|
+
r.is_error = 0
|
|
47
|
+
r
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.error(s)
|
|
51
|
+
r = Tep::MCP::Result.new
|
|
52
|
+
r.text = s
|
|
53
|
+
r.is_error = 1
|
|
54
|
+
r
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Resource read outcome -- a (uri, mime, text) triple wrapped
|
|
58
|
+
# in the resources/read response envelope. Kept as a simple
|
|
59
|
+
# value class (parallel to Result) so spinel tracks the slot
|
|
60
|
+
# types cleanly across module boundaries.
|
|
61
|
+
class ResourceContent
|
|
62
|
+
attr_accessor :uri, :mime, :text
|
|
63
|
+
|
|
64
|
+
def initialize
|
|
65
|
+
@uri = ""
|
|
66
|
+
@mime = "text/plain"
|
|
67
|
+
@text = ""
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Build a text-mime resource content block. URI is the
|
|
72
|
+
# resource's identifier (echoed back to the client so clients
|
|
73
|
+
# can correlate the response with the request).
|
|
74
|
+
def self.resource_text(uri, text)
|
|
75
|
+
c = Tep::MCP::ResourceContent.new
|
|
76
|
+
c.uri = uri
|
|
77
|
+
c.mime = "text/plain"
|
|
78
|
+
c.text = text
|
|
79
|
+
c
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Pull a nested JSON value out of `json` by top-level key,
|
|
83
|
+
# returning the value's JSON-string form. Used by the
|
|
84
|
+
# translator-emitted dispatcher to dig `params` out of the
|
|
85
|
+
# JSON-RPC envelope, then `arguments` out of params, before
|
|
86
|
+
# handing the arguments sub-object to the per-tool cmeth.
|
|
87
|
+
#
|
|
88
|
+
# Returns "{}" when the key isn't present (so downstream
|
|
89
|
+
# Tep::Json.get_str / get_int calls see an empty object that
|
|
90
|
+
# returns their zero-default cleanly).
|
|
91
|
+
def self.nested_extract(json, key)
|
|
92
|
+
pos = Tep::Json.find_value_start(json, key)
|
|
93
|
+
if pos < 0
|
|
94
|
+
return "{}"
|
|
95
|
+
end
|
|
96
|
+
end_pos = Tep::Json.skip_value(json, pos)
|
|
97
|
+
if end_pos <= pos
|
|
98
|
+
return "{}"
|
|
99
|
+
end
|
|
100
|
+
json[pos, end_pos - pos]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# JSON-RPC 2.0 response envelope for `initialize`. The MCP
|
|
104
|
+
# client expects serverInfo + capabilities + protocolVersion.
|
|
105
|
+
# capabilities lists which method groups this server speaks.
|
|
106
|
+
def self.initialize_envelope(req_id, server_name, server_version)
|
|
107
|
+
"{\"jsonrpc\":\"2.0\",\"id\":" + req_id.to_s + "," +
|
|
108
|
+
"\"result\":{" +
|
|
109
|
+
"\"protocolVersion\":\"" + Tep::MCP::PROTOCOL_VERSION + "\"," +
|
|
110
|
+
"\"capabilities\":{\"tools\":{},\"resources\":{}}," +
|
|
111
|
+
"\"serverInfo\":{" +
|
|
112
|
+
"\"name\":" + Tep::Json.quote(server_name) + "," +
|
|
113
|
+
"\"version\":" + Tep::Json.quote(server_version) +
|
|
114
|
+
"}" +
|
|
115
|
+
"}" +
|
|
116
|
+
"}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Wrap a pre-built tools-array JSON string into the tools/list
|
|
120
|
+
# response envelope. tools_array_json is the literal `[{...},
|
|
121
|
+
# {...}]` the translator emits at compile time.
|
|
122
|
+
def self.tools_list_envelope(req_id, tools_array_json)
|
|
123
|
+
"{\"jsonrpc\":\"2.0\",\"id\":" + req_id.to_s + "," +
|
|
124
|
+
"\"result\":{\"tools\":" + tools_array_json + "}" +
|
|
125
|
+
"}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Wrap a tool's text + error-flag into the tools/call response
|
|
129
|
+
# envelope. content is a one-element array with a text block.
|
|
130
|
+
# Takes scalars rather than the Result struct directly so spinel
|
|
131
|
+
# tracks the String param locally through json_quote without
|
|
132
|
+
# going through attr_accessor return-type inference.
|
|
133
|
+
def self.tools_call_envelope(req_id, text, is_error)
|
|
134
|
+
is_err_str = "false"
|
|
135
|
+
if is_error == 1
|
|
136
|
+
is_err_str = "true"
|
|
137
|
+
end
|
|
138
|
+
"{\"jsonrpc\":\"2.0\",\"id\":" + req_id.to_s + "," +
|
|
139
|
+
"\"result\":{" +
|
|
140
|
+
"\"content\":[" +
|
|
141
|
+
"{\"type\":\"text\",\"text\":" + Tep::Json.quote(text) + "}" +
|
|
142
|
+
"]," +
|
|
143
|
+
"\"isError\":" + is_err_str +
|
|
144
|
+
"}" +
|
|
145
|
+
"}"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Wrap a pre-built resources-array JSON string into the
|
|
149
|
+
# resources/list response envelope. Same shape as
|
|
150
|
+
# tools_list_envelope -- translator emits the array literally
|
|
151
|
+
# at compile time so spinel doesn't need to walk it at runtime.
|
|
152
|
+
def self.resources_list_envelope(req_id, resources_array_json)
|
|
153
|
+
"{\"jsonrpc\":\"2.0\",\"id\":" + req_id.to_s + "," +
|
|
154
|
+
"\"result\":{\"resources\":" + resources_array_json + "}" +
|
|
155
|
+
"}"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Wrap a ResourceContent into a resources/read response
|
|
159
|
+
# envelope. contents is a one-element array per MCP spec; the
|
|
160
|
+
# uri / mimeType / text fields are read off as scalars (same
|
|
161
|
+
# spinel-friendly pattern as tools_call_envelope) before being
|
|
162
|
+
# spliced into the JSON.
|
|
163
|
+
def self.resources_read_envelope(req_id, uri, mime, text)
|
|
164
|
+
"{\"jsonrpc\":\"2.0\",\"id\":" + req_id.to_s + "," +
|
|
165
|
+
"\"result\":{\"contents\":[" +
|
|
166
|
+
"{\"uri\":" + Tep::Json.quote(uri) + "," +
|
|
167
|
+
"\"mimeType\":" + Tep::Json.quote(mime) + "," +
|
|
168
|
+
"\"text\":" + Tep::Json.quote(text) + "}" +
|
|
169
|
+
"]}" +
|
|
170
|
+
"}"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Error envelope for resources/read on an unknown URI. Same
|
|
174
|
+
# JSON-RPC code as unknown_tool (-32602 invalid params).
|
|
175
|
+
def self.unknown_resource_envelope(req_id, uri)
|
|
176
|
+
"{\"jsonrpc\":\"2.0\",\"id\":" + req_id.to_s + "," +
|
|
177
|
+
"\"error\":{\"code\":-32602," +
|
|
178
|
+
"\"message\":" + Tep::Json.quote("unknown resource: " + uri) +
|
|
179
|
+
"}" +
|
|
180
|
+
"}"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Error envelope for tools/call on an unknown tool name. Sent
|
|
184
|
+
# as a JSON-RPC error (-32602 invalid params) per the spec.
|
|
185
|
+
def self.unknown_tool_envelope(req_id, tool_name)
|
|
186
|
+
"{\"jsonrpc\":\"2.0\",\"id\":" + req_id.to_s + "," +
|
|
187
|
+
"\"error\":{\"code\":-32602," +
|
|
188
|
+
"\"message\":" + Tep::Json.quote("unknown tool: " + tool_name) +
|
|
189
|
+
"}" +
|
|
190
|
+
"}"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Error envelope for an unrecognized JSON-RPC method. Spec
|
|
194
|
+
# code -32601 (method not found).
|
|
195
|
+
def self.method_not_found_envelope(req_id, method_name)
|
|
196
|
+
"{\"jsonrpc\":\"2.0\",\"id\":" + req_id.to_s + "," +
|
|
197
|
+
"\"error\":{\"code\":-32601," +
|
|
198
|
+
"\"message\":" + Tep::Json.quote("method not found: " + method_name) +
|
|
199
|
+
"}" +
|
|
200
|
+
"}"
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Tep::Multipart -- text-field parsing for multipart/form-data bodies.
|
|
2
|
+
#
|
|
3
|
+
# Browser forms submitted via `new FormData(form)` (or any form
|
|
4
|
+
# carrying file inputs) use `Content-Type: multipart/form-data`
|
|
5
|
+
# instead of urlencoded. tep's request layer treats those bodies
|
|
6
|
+
# as String fields here. File-upload parts (any part with a
|
|
7
|
+
# `filename=` header) are skipped in v1 -- the field's bytes
|
|
8
|
+
# don't land in `req.params`. Supporting file uploads needs a
|
|
9
|
+
# different surface (likely `req.files`) plus an NUL-safe byte
|
|
10
|
+
# array, both follow-ups.
|
|
11
|
+
#
|
|
12
|
+
# Public API mirrors Url.parse_query: pass the raw body + the
|
|
13
|
+
# request's Content-Type header value; get back a string-keyed
|
|
14
|
+
# string-valued hash, ready to merge into `req.params`.
|
|
15
|
+
module Tep
|
|
16
|
+
module Multipart
|
|
17
|
+
# Parse `body` against the boundary embedded in `content_type`.
|
|
18
|
+
# Returns an empty hash when the boundary can't be extracted
|
|
19
|
+
# (defensive: caller already checked `req.multipart?`).
|
|
20
|
+
def self.parse(body, content_type)
|
|
21
|
+
h = Tep.str_hash
|
|
22
|
+
bnd = Tep::Multipart.extract_boundary(content_type)
|
|
23
|
+
if bnd.length == 0
|
|
24
|
+
return h
|
|
25
|
+
end
|
|
26
|
+
delim = "--" + bnd
|
|
27
|
+
parts = body.split(delim)
|
|
28
|
+
i = 1 # parts[0] is the prologue before the first delimiter
|
|
29
|
+
while i < parts.length
|
|
30
|
+
part = parts[i]
|
|
31
|
+
# Terminator: a part that starts with "--" is the closing
|
|
32
|
+
# boundary "--<bnd>--<crlf>"; nothing meaningful after it.
|
|
33
|
+
if part.length >= 2 && part[0, 2] == "--"
|
|
34
|
+
return h
|
|
35
|
+
end
|
|
36
|
+
# Strip the CRLF that follows every interior delimiter.
|
|
37
|
+
if part.length >= 2 && part[0, 2] == "\r\n"
|
|
38
|
+
part = part[2, part.length - 2]
|
|
39
|
+
end
|
|
40
|
+
# Strip the CRLF that precedes the next delimiter (every
|
|
41
|
+
# interior part ends with \r\n before the next "--<bnd>").
|
|
42
|
+
if part.length >= 2 && part[part.length - 2, 2] == "\r\n"
|
|
43
|
+
part = part[0, part.length - 2]
|
|
44
|
+
end
|
|
45
|
+
sep = Tep.str_find(part, "\r\n\r\n", 0)
|
|
46
|
+
if sep >= 0
|
|
47
|
+
headers = part[0, sep]
|
|
48
|
+
value = part[sep + 4, part.length - sep - 4]
|
|
49
|
+
name = Tep::Multipart.extract_field_name(headers)
|
|
50
|
+
has_filename = Tep.str_find(headers, "filename=", 0) >= 0
|
|
51
|
+
if name.length > 0 && !has_filename
|
|
52
|
+
h[name] = value
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
i += 1
|
|
56
|
+
end
|
|
57
|
+
h
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Extract `boundary=...` from a Content-Type value. Handles
|
|
61
|
+
# quoted (`boundary="x"`) and unquoted (`boundary=x;` or
|
|
62
|
+
# `boundary=x` at end of string).
|
|
63
|
+
def self.extract_boundary(content_type)
|
|
64
|
+
at = Tep.str_find(content_type, "boundary=", 0)
|
|
65
|
+
if at < 0
|
|
66
|
+
return ""
|
|
67
|
+
end
|
|
68
|
+
rest = content_type[at + 9, content_type.length - at - 9]
|
|
69
|
+
if rest.length > 0 && rest[0, 1] == "\""
|
|
70
|
+
end_q = Tep.str_find(rest, "\"", 1)
|
|
71
|
+
if end_q < 0
|
|
72
|
+
return ""
|
|
73
|
+
end
|
|
74
|
+
return rest[1, end_q - 1]
|
|
75
|
+
end
|
|
76
|
+
semi = Tep.str_find(rest, ";", 0)
|
|
77
|
+
if semi >= 0
|
|
78
|
+
return rest[0, semi]
|
|
79
|
+
end
|
|
80
|
+
rest
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Extract the `name="..."` value from a part's headers blob.
|
|
84
|
+
# Returns "" when no name found.
|
|
85
|
+
def self.extract_field_name(headers)
|
|
86
|
+
at = Tep.str_find(headers, "name=\"", 0)
|
|
87
|
+
if at < 0
|
|
88
|
+
return ""
|
|
89
|
+
end
|
|
90
|
+
start = at + 6
|
|
91
|
+
end_q = Tep.str_find(headers, "\"", start)
|
|
92
|
+
if end_q < 0
|
|
93
|
+
return ""
|
|
94
|
+
end
|
|
95
|
+
headers[start, end_q - start]
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
data/lib/tep/net.rb
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# All FFI plumbing lives at the top level so spinel's name resolver
|
|
2
|
+
# finds it from anywhere in the Tep tree (nested modules confuse it).
|
|
3
|
+
#
|
|
4
|
+
# The `@TEP_SPHTTP_O@` placeholder is substituted by `bin/tep` (or
|
|
5
|
+
# the Makefile) with the absolute path to the built sphttp.o on the
|
|
6
|
+
# current host. Spinel doesn't support `__dir__` or `ENV.fetch` in
|
|
7
|
+
# top-level ffi_cflags, so a build-time substitution is the cleanest
|
|
8
|
+
# portable shape.
|
|
9
|
+
module Sock
|
|
10
|
+
ffi_cflags "@TEP_SPHTTP_O@"
|
|
11
|
+
# Outbound TLS (sphttp_connect_tls) is backed by the system
|
|
12
|
+
# libssl/libcrypto. Linked for every app (like sqlite3 elsewhere);
|
|
13
|
+
# the plaintext path never calls into it, so apps that make no HTTPS
|
|
14
|
+
# requests pay only the link cost, not runtime. See tep#148.
|
|
15
|
+
# (When OpenSSL is off the default path -- macOS/Homebrew -- the build
|
|
16
|
+
# finds it via CPATH/LIBRARY_PATH in the environment, not a cflag
|
|
17
|
+
# here; spinel's ffi_cflags rejects an empty-string placeholder.)
|
|
18
|
+
ffi_lib "ssl"
|
|
19
|
+
ffi_lib "crypto"
|
|
20
|
+
|
|
21
|
+
ffi_func :sphttp_listen, [:int, :int], :int
|
|
22
|
+
ffi_func :sphttp_accept, [:int], :int
|
|
23
|
+
# Non-blocking accept variant used by Tep::Server::Scheduled.
|
|
24
|
+
# Listen fd must be in non-blocking mode (sphttp_set_nonblock).
|
|
25
|
+
# Returns -1 with errno EAGAIN/EWOULDBLOCK if no pending connection.
|
|
26
|
+
ffi_func :sphttp_accept_nb, [:int], :int
|
|
27
|
+
ffi_func :sphttp_read_request, [:int], :int
|
|
28
|
+
ffi_func :sphttp_request_buf, [], :str
|
|
29
|
+
ffi_func :sphttp_request_len, [], :int
|
|
30
|
+
ffi_func :sphttp_drain_body, [:int, :int], :str
|
|
31
|
+
ffi_func :sphttp_write_str, [:int, :str], :int
|
|
32
|
+
|
|
33
|
+
# Binary-safe write + recv pair, used by Tep::WebSocket (and any
|
|
34
|
+
# other caller that needs to send/receive bytes containing 0x00).
|
|
35
|
+
# The recv side mirrors the request_buf / _len accessor pattern.
|
|
36
|
+
# See sphttp.c for the binary-safety contract.
|
|
37
|
+
ffi_func :sphttp_write_bytes, [:int, :str, :int], :int
|
|
38
|
+
ffi_func :sphttp_recv_into_frame, [:int], :int
|
|
39
|
+
ffi_func :sphttp_recv_frame_buf, [], :str
|
|
40
|
+
ffi_func :sphttp_recv_frame_len, [], :int
|
|
41
|
+
|
|
42
|
+
# Shutdown-on-signal plumbing. Install once at server start;
|
|
43
|
+
# sphttp_shutdown_requested polls the flag (sigaction sets it on
|
|
44
|
+
# SIGTERM/SIGINT). The server's accept loop checks the flag after
|
|
45
|
+
# sphttp_accept returns -1 and runs Tep.on_shutdown before exit.
|
|
46
|
+
ffi_func :sphttp_install_term_handlers, [], :int
|
|
47
|
+
ffi_func :sphttp_shutdown_requested, [], :int
|
|
48
|
+
|
|
49
|
+
# Millisecond sleep helper for sub-second pacing. spinel's
|
|
50
|
+
# Tep::Scheduler.pause is integer-second only; this exposes the
|
|
51
|
+
# POSIX nanosleep path. Returns 0 on success, -1 on EINTR. Used by
|
|
52
|
+
# Tep::Proxy's retry-backoff loop.
|
|
53
|
+
ffi_func :sphttp_sleep_ms, [:int], :int
|
|
54
|
+
|
|
55
|
+
# HTTP/1.1 outbound connection pool (chunk 6.7a). Per-process pool
|
|
56
|
+
# keyed by (host, port). checkout returns an idle fd or -1; checkin
|
|
57
|
+
# registers one; close_idle sweeps entries older than idle_seconds.
|
|
58
|
+
# Stat getters fetch one counter each (avoids a struct-return over
|
|
59
|
+
# FFI). See Tep::Http::Pool for the Ruby surface.
|
|
60
|
+
ffi_func :sphttp_pool_checkout, [:str, :int], :int
|
|
61
|
+
ffi_func :sphttp_pool_checkin, [:int, :str, :int], :int
|
|
62
|
+
ffi_func :sphttp_pool_close_idle, [:int], :int
|
|
63
|
+
ffi_func :sphttp_pool_stat_checkouts, [], :int
|
|
64
|
+
ffi_func :sphttp_pool_stat_checkins, [], :int
|
|
65
|
+
ffi_func :sphttp_pool_stat_hits, [], :int
|
|
66
|
+
ffi_func :sphttp_pool_stat_misses, [], :int
|
|
67
|
+
|
|
68
|
+
# uname-based host introspection for the toy/v1 envelope (see
|
|
69
|
+
# docs/events-schema.md). sphttp_os_kind returns lowercased
|
|
70
|
+
# uname.sysname ("linux" / "darwin" / ...); sphttp_arch_kind
|
|
71
|
+
# returns uname.machine as-is ("aarch64" / "x86_64" / ...). Both
|
|
72
|
+
# return "unknown" on uname() failure.
|
|
73
|
+
ffi_func :sphttp_os_kind, [], :str
|
|
74
|
+
ffi_func :sphttp_arch_kind, [], :str
|
|
75
|
+
|
|
76
|
+
# ISO-8601 UTC timestamp for an epoch-seconds value. Used by
|
|
77
|
+
# Tep::Events (toy/v1 envelope) for run_start/run_end wall-clock
|
|
78
|
+
# fields -- spinel's Time.now is integer-epoch only.
|
|
79
|
+
ffi_func :sphttp_iso8601_utc, [:int], :str
|
|
80
|
+
# RFC 1123 GMT date (HTTP Date / Last-Modified / Expires) <-> epoch.
|
|
81
|
+
# parse returns -1 if the string doesn't parse.
|
|
82
|
+
ffi_func :sphttp_http_date, [:int], :str
|
|
83
|
+
ffi_func :sphttp_parse_http_date, [:str], :int
|
|
84
|
+
|
|
85
|
+
ffi_func :sphttp_sendfile, [:int, :str], :int
|
|
86
|
+
ffi_func :sphttp_filesize, [:str], :int
|
|
87
|
+
ffi_func :sphttp_file_mtime, [:str], :int
|
|
88
|
+
ffi_func :sphttp_close, [:int], :int
|
|
89
|
+
ffi_func :sphttp_fork, [], :int
|
|
90
|
+
ffi_func :sphttp_exit, [:int], :int
|
|
91
|
+
ffi_func :sphttp_getpid, [], :int
|
|
92
|
+
ffi_func :sphttp_wait_any, [], :int
|
|
93
|
+
ffi_func :sphttp_write_chunk, [:int, :str], :int
|
|
94
|
+
ffi_func :sphttp_write_chunk_end, [:int], :int
|
|
95
|
+
|
|
96
|
+
# Poll-based I/O readiness, used by Tep::Scheduler.io_wait. Mode
|
|
97
|
+
# bits in/out: 1=READ, 2=WRITE.
|
|
98
|
+
ffi_func :sphttp_poll_reset, [], :int
|
|
99
|
+
ffi_func :sphttp_poll_add, [:int, :int], :int
|
|
100
|
+
ffi_func :sphttp_poll_run, [:int], :int
|
|
101
|
+
ffi_func :sphttp_poll_ready, [:int], :int
|
|
102
|
+
ffi_func :sphttp_set_nonblock, [:int], :int
|
|
103
|
+
# Bound a blocking recv (SO_RCVTIMEO, ms). Used by the pooled
|
|
104
|
+
# outbound client so a no-Content-Length keep-alive response can't
|
|
105
|
+
# hang the worker. 0 clears the timeout.
|
|
106
|
+
ffi_func :sphttp_set_recv_timeout, [:int, :int], :int
|
|
107
|
+
|
|
108
|
+
# Outbound TCP for clients (Tep::Http, etc.).
|
|
109
|
+
ffi_func :sphttp_connect, [:str, :int], :int
|
|
110
|
+
# TLS variant: TCP connect + verified TLS handshake (SNI + hostname
|
|
111
|
+
# + peer cert). Returns an fd whose write/recv/close transparently
|
|
112
|
+
# route through the SSL*. -1 on connect/handshake/verify failure.
|
|
113
|
+
ffi_func :sphttp_connect_tls, [:str, :int], :int
|
|
114
|
+
# Inbound (server) TLS (tep#148 phase 2). server_init loads cert+key
|
|
115
|
+
# once (before prefork); accept_tls wraps an accepted fd in a TLS
|
|
116
|
+
# handshake (0 ok / -1 fail, caller closes). read/write/close then
|
|
117
|
+
# route through the SSL* via the same fd registry.
|
|
118
|
+
ffi_func :sphttp_tls_server_init, [:str, :str], :int
|
|
119
|
+
ffi_func :sphttp_accept_tls, [:int], :int
|
|
120
|
+
# Non-blocking TLS (tep#150 outbound coop + scheduled inbound). *_start
|
|
121
|
+
# sets up the SSL but does NOT run the handshake; handshake_step drives
|
|
122
|
+
# one SSL_do_handshake (0=done / 1=want-read / 2=want-write / -1=fail)
|
|
123
|
+
# so a fiber parks on io_wait between steps. io_status reports the last
|
|
124
|
+
# recv/handshake want-state (0 ok / 1 read / 2 write / 3 eof / -1 err)
|
|
125
|
+
# so the coop recv loops tell a TLS partial record from a real EOF.
|
|
126
|
+
ffi_func :sphttp_tls_connect_start, [:str, :int], :int
|
|
127
|
+
ffi_func :sphttp_tls_accept_start, [:int], :int
|
|
128
|
+
ffi_func :sphttp_tls_handshake_step, [:int], :int
|
|
129
|
+
ffi_func :sphttp_io_status, [], :int
|
|
130
|
+
ffi_func :sphttp_recv_some, [:int, :int], :str
|
|
131
|
+
ffi_func :sphttp_recv_all, [:int, :int], :str
|
|
132
|
+
|
|
133
|
+
# popen-shaped shell capture used by Tep::Shell.run. File I/O goes
|
|
134
|
+
# through spinel's built-in File.read / File.write since master
|
|
135
|
+
# (matz/spinel#505 made File.write binary-safe).
|
|
136
|
+
ffi_func :sphttp_shell_capture, [:str, :int], :str
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Crypto FFI -- SHA-256/HMAC/PBKDF2/B64URL/random. Symbols live in
|
|
140
|
+
# spinel's libspinel_rt.a (added upstream as lib/sp_crypto.c via
|
|
141
|
+
# matz/spinel#514), which the spinel driver auto-links into every
|
|
142
|
+
# binary. No ffi_cflags needed; just declare the signatures.
|
|
143
|
+
module Crypto
|
|
144
|
+
ffi_func :sp_crypto_hmac_sha256_hex, [:str, :str], :str
|
|
145
|
+
ffi_func :sp_crypto_hmac_sha256_b64url, [:str, :str], :str
|
|
146
|
+
ffi_func :sp_crypto_b64url_encode, [:str], :str
|
|
147
|
+
ffi_func :sp_crypto_b64url_decode, [:str], :str
|
|
148
|
+
ffi_func :sp_crypto_pbkdf2_sha256_b64url, [:str, :str, :int], :str
|
|
149
|
+
ffi_func :sp_crypto_random_b64url, [:int], :str
|
|
150
|
+
# SHA-1 + WebSocket accept-key compute. SHA-1 is shipped only
|
|
151
|
+
# because RFC 6455 requires it for the Sec-WebSocket-Accept
|
|
152
|
+
# derivation; do NOT use it for anything else (collision-broken).
|
|
153
|
+
ffi_func :sp_crypto_sha1_hex, [:str], :str
|
|
154
|
+
ffi_func :sp_crypto_websocket_accept, [:str], :str
|
|
155
|
+
end
|