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/server.rb
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# Tep::Server -- accept loop. Single-threaded inside one process;
|
|
2
|
+
# the perf model is "fork N workers, each runs its own Server"
|
|
3
|
+
# (-w workers) using SO_REUSEPORT so the kernel load-balances.
|
|
4
|
+
module Tep
|
|
5
|
+
def self.reason(status)
|
|
6
|
+
if status == 200; return "OK"; end
|
|
7
|
+
if status == 201; return "Created"; end
|
|
8
|
+
if status == 204; return "No Content"; end
|
|
9
|
+
if status == 301; return "Moved Permanently"; end
|
|
10
|
+
if status == 302; return "Found"; end
|
|
11
|
+
if status == 304; return "Not Modified"; end
|
|
12
|
+
if status == 400; return "Bad Request"; end
|
|
13
|
+
if status == 401; return "Unauthorized"; end
|
|
14
|
+
if status == 403; return "Forbidden"; end
|
|
15
|
+
if status == 404; return "Not Found"; end
|
|
16
|
+
if status == 500; return "Internal Server Error"; end
|
|
17
|
+
"OK"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class Server
|
|
21
|
+
attr_accessor :app
|
|
22
|
+
|
|
23
|
+
def initialize(app)
|
|
24
|
+
@app = app
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def run(port, workers, quiet)
|
|
28
|
+
scheme = "http"
|
|
29
|
+
if Tep::APP.tls_cert.length > 0
|
|
30
|
+
scheme = "https"
|
|
31
|
+
end
|
|
32
|
+
if !quiet
|
|
33
|
+
puts "[tep " + VERSION + "] listening on " + scheme + "://0.0.0.0:" + port.to_s +
|
|
34
|
+
" (workers=" + workers.to_s + ")"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Install SIGTERM/SIGINT handlers BEFORE fork so children inherit
|
|
38
|
+
# them; on signal, sphttp_accept returns -1 and the worker loop
|
|
39
|
+
# runs Tep.on_shutdown (flushes events.run_end + future hooks).
|
|
40
|
+
Sock.sphttp_install_term_handlers
|
|
41
|
+
|
|
42
|
+
# Inbound TLS (tep#148 phase 2): load the server cert/key once
|
|
43
|
+
# before forking so every worker inherits the SSL_CTX. A bad
|
|
44
|
+
# cert/key is fatal -- fail loud rather than serve plaintext on a
|
|
45
|
+
# port the operator believes is TLS.
|
|
46
|
+
if Tep::APP.tls_cert.length > 0 && Tep::APP.tls_key.length > 0
|
|
47
|
+
if Sock.sphttp_tls_server_init(Tep::APP.tls_cert, Tep::APP.tls_key) < 0
|
|
48
|
+
puts "tep: TLS init failed (cert=" + Tep::APP.tls_cert + ", key=" + Tep::APP.tls_key + ")"
|
|
49
|
+
exit(1)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if workers <= 1
|
|
54
|
+
sfd = Sock.sphttp_listen(port, 0)
|
|
55
|
+
if sfd < 0
|
|
56
|
+
puts "tep: bind failed on :" + port.to_s
|
|
57
|
+
exit(1)
|
|
58
|
+
end
|
|
59
|
+
worker_loop(sfd)
|
|
60
|
+
# Single-process is its own "parent" -- emit run_end here.
|
|
61
|
+
if Sock.sphttp_shutdown_requested != 0
|
|
62
|
+
Tep.on_shutdown
|
|
63
|
+
end
|
|
64
|
+
return
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Pre-fork. Each child opens its own SO_REUSEPORT listener so
|
|
68
|
+
# the kernel load-balances accept() across workers.
|
|
69
|
+
i = 0
|
|
70
|
+
while i < workers
|
|
71
|
+
pid = Sock.sphttp_fork
|
|
72
|
+
if pid == 0
|
|
73
|
+
sfd = Sock.sphttp_listen(port, 1)
|
|
74
|
+
if sfd < 0
|
|
75
|
+
puts "tep: worker " + Sock.sphttp_getpid.to_s + " bind failed"
|
|
76
|
+
exit(1)
|
|
77
|
+
end
|
|
78
|
+
worker_loop(sfd)
|
|
79
|
+
exit(0)
|
|
80
|
+
end
|
|
81
|
+
i += 1
|
|
82
|
+
end
|
|
83
|
+
# Parent: reap children until none remain (wait returns -1).
|
|
84
|
+
# On SIGTERM-to-the-pgroup, children break their accept loops and
|
|
85
|
+
# exit; parent's wait_any reaps them in order. Once all workers
|
|
86
|
+
# are gone, emit the single aggregated run_end (re-reading the
|
|
87
|
+
# JSONL for cross-worker stats; see Tep::Events#run_end_aggregated).
|
|
88
|
+
loop do
|
|
89
|
+
gone = Sock.sphttp_wait_any
|
|
90
|
+
if gone < 0
|
|
91
|
+
break
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
if Sock.sphttp_shutdown_requested != 0
|
|
95
|
+
Tep.on_shutdown
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def worker_loop(sfd)
|
|
100
|
+
loop do
|
|
101
|
+
client = Sock.sphttp_accept(sfd)
|
|
102
|
+
if client < 0
|
|
103
|
+
# accept returns -1 with the term flag set after the first
|
|
104
|
+
# SIGTERM/SIGINT (sphttp_accept retries past unrelated
|
|
105
|
+
# signals). Break here; the parent (or this same process for
|
|
106
|
+
# workers=1) emits the aggregated run_end. Workers used to
|
|
107
|
+
# call Tep.on_shutdown here too, which emitted N run_ends
|
|
108
|
+
# in the JSONL for an N-worker deployment (#128).
|
|
109
|
+
if Sock.sphttp_shutdown_requested != 0
|
|
110
|
+
break
|
|
111
|
+
end
|
|
112
|
+
next
|
|
113
|
+
end
|
|
114
|
+
# Inbound TLS: complete the server-side handshake on the freshly
|
|
115
|
+
# accepted fd before reading the request. Bound the handshake
|
|
116
|
+
# with a recv timeout first -- otherwise a connection that opens
|
|
117
|
+
# but never sends a ClientHello (a port probe, a slowloris, a
|
|
118
|
+
# plain-HTTP client) blocks SSL_accept and wedges this worker.
|
|
119
|
+
# Clear the timeout after a successful handshake so normal
|
|
120
|
+
# (incl. keep-alive) request reads aren't bounded. On failure
|
|
121
|
+
# drop the connection.
|
|
122
|
+
if Tep::APP.tls_cert.length > 0
|
|
123
|
+
Sock.sphttp_set_recv_timeout(client, 5000)
|
|
124
|
+
if Sock.sphttp_accept_tls(client) < 0
|
|
125
|
+
Sock.sphttp_close(client)
|
|
126
|
+
next
|
|
127
|
+
end
|
|
128
|
+
Sock.sphttp_set_recv_timeout(client, 0)
|
|
129
|
+
end
|
|
130
|
+
handle_connection(client)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Keep-alive loop on a single accepted connection.
|
|
135
|
+
def handle_connection(client)
|
|
136
|
+
keep_going = true
|
|
137
|
+
while keep_going
|
|
138
|
+
n = Sock.sphttp_read_request(client)
|
|
139
|
+
if n <= 0
|
|
140
|
+
break
|
|
141
|
+
end
|
|
142
|
+
blob = Sock.sphttp_request_buf
|
|
143
|
+
req = Parser.parse(blob)
|
|
144
|
+
if req == nil
|
|
145
|
+
send_simple(client, 400, "bad request")
|
|
146
|
+
break
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
req.consume_body(client)
|
|
150
|
+
|
|
151
|
+
res = Response.new
|
|
152
|
+
@app.dispatch(req, res)
|
|
153
|
+
|
|
154
|
+
keep_alive = req.keep_alive? && !res.halted_close?
|
|
155
|
+
write_response(client, req, res, keep_alive)
|
|
156
|
+
keep_going = keep_alive
|
|
157
|
+
end
|
|
158
|
+
Sock.sphttp_close(client)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def write_response(client, req, res, keep_alive)
|
|
162
|
+
# WebSocket upgrade is only supported under Tep::Server::Scheduled
|
|
163
|
+
# (the recv loop needs cooperative I/O via Tep::Scheduler.io_wait).
|
|
164
|
+
# Under the prefork-blocking server, fail with 501 so the client
|
|
165
|
+
# sees a clean refusal instead of a half-installed handshake.
|
|
166
|
+
if res.upgrading_ws
|
|
167
|
+
send_simple(client, 501, "WebSocket requires the scheduled server: set :scheduler, :scheduled")
|
|
168
|
+
return
|
|
169
|
+
end
|
|
170
|
+
if res.streaming
|
|
171
|
+
# Chunked-encoding stream. Send headers immediately, hand a
|
|
172
|
+
# Stream writer to the user's Streamer.pump, emit terminator.
|
|
173
|
+
# Connection is closed afterwards (keep-alive + chunked is
|
|
174
|
+
# technically legal but we keep things simple).
|
|
175
|
+
res.headers["Transfer-Encoding"] = "chunked"
|
|
176
|
+
res.headers["Connection"] = "close"
|
|
177
|
+
if !res.headers.key?("Content-Type")
|
|
178
|
+
res.headers["Content-Type"] = "text/event-stream"
|
|
179
|
+
end
|
|
180
|
+
head = build_head(req, res)
|
|
181
|
+
Sock.sphttp_write_str(client, head)
|
|
182
|
+
out = Stream.new(client)
|
|
183
|
+
res.streamer.pump(out)
|
|
184
|
+
Sock.sphttp_write_chunk_end(client)
|
|
185
|
+
return
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
if res.file_path.length > 0
|
|
189
|
+
# send_file path -- compute size, emit headers, then stream.
|
|
190
|
+
sz = Sock.sphttp_filesize(res.file_path)
|
|
191
|
+
if sz < 0
|
|
192
|
+
send_simple(client, 404, "file not found")
|
|
193
|
+
return
|
|
194
|
+
end
|
|
195
|
+
# Cache validators for revalidation (#152): a size-mtime ETag +
|
|
196
|
+
# Last-Modified, set before the conditional check.
|
|
197
|
+
mt = Sock.sphttp_file_mtime(res.file_path)
|
|
198
|
+
if mt >= 0
|
|
199
|
+
res.etag(sz.to_s + "-" + mt.to_s)
|
|
200
|
+
res.last_modified(mt)
|
|
201
|
+
end
|
|
202
|
+
if !res.headers.key?("Content-Type")
|
|
203
|
+
res.headers["Content-Type"] = "application/octet-stream"
|
|
204
|
+
end
|
|
205
|
+
if keep_alive
|
|
206
|
+
res.headers["Connection"] = "keep-alive"
|
|
207
|
+
else
|
|
208
|
+
res.headers["Connection"] = "close"
|
|
209
|
+
end
|
|
210
|
+
# Conditional GET: 304 + no body (no sendfile) when the client's
|
|
211
|
+
# cached copy is still fresh.
|
|
212
|
+
if Tep::Cache.not_modified?(req, res)
|
|
213
|
+
res.set_status(304)
|
|
214
|
+
res.headers["Content-Length"] = "0"
|
|
215
|
+
Sock.sphttp_write_str(client, build_head(req, res))
|
|
216
|
+
return
|
|
217
|
+
end
|
|
218
|
+
res.headers["Content-Length"] = sz.to_s
|
|
219
|
+
head = build_head(req, res)
|
|
220
|
+
Sock.sphttp_write_str(client, head)
|
|
221
|
+
Sock.sphttp_sendfile(client, res.file_path)
|
|
222
|
+
return
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Conditional GET: if the handler set a validator (ETag /
|
|
226
|
+
# Last-Modified) that the request's precondition satisfies, drop to
|
|
227
|
+
# 304 with no body. The validator + cache headers already on res
|
|
228
|
+
# are still emitted; Content-Length becomes 0 below. (Issue #152.)
|
|
229
|
+
if Tep::Cache.not_modified?(req, res)
|
|
230
|
+
res.set_status(304)
|
|
231
|
+
res.set_body("")
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
if res.body.length > 0 && !res.headers.key?("Content-Type")
|
|
235
|
+
res.headers["Content-Type"] = "text/html; charset=utf-8"
|
|
236
|
+
end
|
|
237
|
+
res.headers["Content-Length"] = res.body.length.to_s
|
|
238
|
+
if keep_alive
|
|
239
|
+
res.headers["Connection"] = "keep-alive"
|
|
240
|
+
else
|
|
241
|
+
res.headers["Connection"] = "close"
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
head = build_head(req, res)
|
|
245
|
+
Sock.sphttp_write_str(client, head)
|
|
246
|
+
if res.body.length > 0
|
|
247
|
+
Sock.sphttp_write_str(client, res.body)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def build_head(req, res)
|
|
252
|
+
reason = Tep.reason(res.status)
|
|
253
|
+
head = req.http_version + " " + res.status.to_s + " " + reason + "\r\n"
|
|
254
|
+
res.headers.each do |k, v|
|
|
255
|
+
head = head + k + ": " + v + "\r\n"
|
|
256
|
+
end
|
|
257
|
+
# Set-Cookie can repeat; emit each on its own line.
|
|
258
|
+
ci = 0
|
|
259
|
+
while ci < res.set_cookies.length
|
|
260
|
+
head = head + "Set-Cookie: " + res.set_cookies[ci] + "\r\n"
|
|
261
|
+
ci += 1
|
|
262
|
+
end
|
|
263
|
+
head + "\r\n"
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def send_simple(client, status, msg)
|
|
267
|
+
reason = Tep.reason(status)
|
|
268
|
+
body = "<h1>" + status.to_s + " " + reason + "</h1><p>" + msg + "</p>\n"
|
|
269
|
+
head = "HTTP/1.0 " + status.to_s + " " + reason + "\r\n" +
|
|
270
|
+
"Content-Type: text/html; charset=utf-8\r\n" +
|
|
271
|
+
"Content-Length: " + body.length.to_s + "\r\n" +
|
|
272
|
+
"Connection: close\r\n\r\n"
|
|
273
|
+
Sock.sphttp_write_str(client, head + body)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# Tep::Server::Scheduled -- Falcon-shape fiber-per-connection HTTP
|
|
2
|
+
# server, built on Tep::Scheduler + sphttp non-blocking accept/recv.
|
|
3
|
+
#
|
|
4
|
+
# Why this exists
|
|
5
|
+
# ---------------
|
|
6
|
+
# The default Tep::Server (in server.rb) is prefork + blocking per
|
|
7
|
+
# worker -- N workers <=> N concurrent connections. WebSockets and
|
|
8
|
+
# slow keep-alive clients tie up a worker for the full connection
|
|
9
|
+
# duration, so the prefork pool's effective concurrency degrades
|
|
10
|
+
# to N regardless of actual CPU work. The scheduled variant accepts
|
|
11
|
+
# in a fiber, spawns one fiber per accepted connection, and parks
|
|
12
|
+
# all I/O on Tep::Scheduler.io_wait -- N workers serve M >> N
|
|
13
|
+
# concurrent connections, bounded only by per-fiber memory.
|
|
14
|
+
#
|
|
15
|
+
# Fiber bodies use ordinary closure capture for sfd / client now
|
|
16
|
+
# (matz/spinel#564 + #1007 both closed; the heap-cell-reset fix
|
|
17
|
+
# in spinel commit 48594d6 lets multi-method capture chains lower
|
|
18
|
+
# correctly). cmeths still preferred for accept_loop /
|
|
19
|
+
# handle_connection so the bodies read cleanly without per-instance
|
|
20
|
+
# state, but the per-connection fd flows through closure capture,
|
|
21
|
+
# not the earlier `Tep::APP.pending_*` stash + pause(0) handoff.
|
|
22
|
+
module Tep
|
|
23
|
+
class Server
|
|
24
|
+
class Scheduled
|
|
25
|
+
# Max bytes accepted from a single request's start-line +
|
|
26
|
+
# headers. Bigger requests get 413; matches the blocking
|
|
27
|
+
# server's SPHTTP_BUFSIZE cap (64 KiB).
|
|
28
|
+
MAX_REQUEST_BYTES = 65535
|
|
29
|
+
|
|
30
|
+
# Idle keep-alive timeout between requests on the same
|
|
31
|
+
# connection. 30s matches nginx; bump from app code as needed.
|
|
32
|
+
KEEPALIVE_TIMEOUT = 30
|
|
33
|
+
|
|
34
|
+
# Slow-headers DoS guard.
|
|
35
|
+
HEADER_READ_TIMEOUT = 10
|
|
36
|
+
|
|
37
|
+
attr_accessor :app
|
|
38
|
+
|
|
39
|
+
def initialize(app)
|
|
40
|
+
@app = app
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def run(port, workers, quiet)
|
|
44
|
+
sfd = Sock.sphttp_listen(port, workers > 1 ? 1 : 0)
|
|
45
|
+
if sfd < 0
|
|
46
|
+
return 1
|
|
47
|
+
end
|
|
48
|
+
Sock.sphttp_set_nonblock(sfd)
|
|
49
|
+
|
|
50
|
+
# Install SIGTERM/SIGINT handlers BEFORE fork so children
|
|
51
|
+
# inherit them; accept_loop checks the term flag once per
|
|
52
|
+
# second and runs Tep.on_shutdown (run_end + future hooks).
|
|
53
|
+
Sock.sphttp_install_term_handlers
|
|
54
|
+
|
|
55
|
+
# Inbound TLS (tep#148 phase 2, scheduled variant): load the
|
|
56
|
+
# server cert/key once before forking so every worker inherits
|
|
57
|
+
# the SSL_CTX. A bad cert/key is fatal -- never silently serve
|
|
58
|
+
# plaintext on a port the operator believes is TLS. The handshake
|
|
59
|
+
# itself runs non-blocking per-connection (handle_connection).
|
|
60
|
+
if Tep::APP.tls_cert.length > 0 && Tep::APP.tls_key.length > 0
|
|
61
|
+
if Sock.sphttp_tls_server_init(Tep::APP.tls_cert, Tep::APP.tls_key) < 0
|
|
62
|
+
return 1
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if workers > 1
|
|
67
|
+
i = 0
|
|
68
|
+
while i < workers
|
|
69
|
+
pid = Sock.sphttp_fork
|
|
70
|
+
if pid == 0
|
|
71
|
+
Tep::Server::Scheduled.run_worker(sfd)
|
|
72
|
+
Sock.sphttp_exit(0)
|
|
73
|
+
end
|
|
74
|
+
i += 1
|
|
75
|
+
end
|
|
76
|
+
# Reap children until none remain. After all workers exit,
|
|
77
|
+
# emit the single aggregated run_end (see #128 / Tep::Events
|
|
78
|
+
# #run_end_aggregated).
|
|
79
|
+
loop do
|
|
80
|
+
gone = Sock.sphttp_wait_any
|
|
81
|
+
if gone < 0
|
|
82
|
+
break
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
if Sock.sphttp_shutdown_requested != 0
|
|
86
|
+
Tep.on_shutdown
|
|
87
|
+
end
|
|
88
|
+
else
|
|
89
|
+
Tep::Server::Scheduled.run_worker(sfd)
|
|
90
|
+
# Single-process: this IS the parent; emit run_end here.
|
|
91
|
+
if Sock.sphttp_shutdown_requested != 0
|
|
92
|
+
Tep.on_shutdown
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
0
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Spawn the accept fiber + pump the scheduler. Called inside
|
|
99
|
+
# each prefork child. Loops directly on `tick` rather than
|
|
100
|
+
# `run_until_empty` because the accept fiber parks on io_wait
|
|
101
|
+
# indefinitely -- run_until_empty bails when no fiber is ready
|
|
102
|
+
# to resume THIS pass; we need to keep polling so parked
|
|
103
|
+
# accept-on-sfd fibers get woken when a connection arrives.
|
|
104
|
+
def self.run_worker(sfd)
|
|
105
|
+
f = Fiber.new { Tep::Server::Scheduled.accept_loop(sfd) }
|
|
106
|
+
Tep::Scheduler.spawn_fiber(f)
|
|
107
|
+
while Tep::Scheduler.alive_count > 0
|
|
108
|
+
Tep::Scheduler.tick(1000)
|
|
109
|
+
end
|
|
110
|
+
0
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Accept loop. Each accepted connection becomes its own fiber
|
|
114
|
+
# that closes over the just-accepted `client` fd.
|
|
115
|
+
def self.accept_loop(sfd)
|
|
116
|
+
while true
|
|
117
|
+
# SIGTERM/SIGINT: sphttp's term flag is set by the signal
|
|
118
|
+
# handler; check before parking on io_wait so we don't sleep
|
|
119
|
+
# past a shutdown request. The 1s io_wait timeout below
|
|
120
|
+
# bounds the sleep-side latency. The parent (or this same
|
|
121
|
+
# process for workers=1) emits the aggregated run_end after
|
|
122
|
+
# all workers exit (#128).
|
|
123
|
+
if Sock.sphttp_shutdown_requested != 0
|
|
124
|
+
break
|
|
125
|
+
end
|
|
126
|
+
# Bounded wait so the flag check above runs once per second
|
|
127
|
+
# even when traffic is idle (was -1 = wait forever).
|
|
128
|
+
ready = Tep::Scheduler.io_wait(sfd, Tep::Scheduler::READ, 1)
|
|
129
|
+
if ready == 0
|
|
130
|
+
next
|
|
131
|
+
end
|
|
132
|
+
client = Sock.sphttp_accept_nb(sfd)
|
|
133
|
+
if client < 0
|
|
134
|
+
next
|
|
135
|
+
end
|
|
136
|
+
Sock.sphttp_set_nonblock(client)
|
|
137
|
+
conn = Fiber.new { Tep::Server::Scheduled.handle_connection(client) }
|
|
138
|
+
Tep::Scheduler.spawn_fiber(conn)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Non-blocking server-side TLS handshake on an accepted fd.
|
|
143
|
+
# Returns 1 on success (SSL* registered -- reads/writes are now
|
|
144
|
+
# transparent), 0 on failure. Drives SSL_do_handshake, parking on
|
|
145
|
+
# io_wait for the direction OpenSSL asks for, bounded by
|
|
146
|
+
# HEADER_READ_TIMEOUT so a connection that opens but never
|
|
147
|
+
# completes the handshake (port probe, slowloris, plain-HTTP
|
|
148
|
+
# client) can't pin the fiber.
|
|
149
|
+
def self.tls_handshake(client)
|
|
150
|
+
if Sock.sphttp_tls_accept_start(client) < 0
|
|
151
|
+
return 0
|
|
152
|
+
end
|
|
153
|
+
deadline = Time.now.to_i + HEADER_READ_TIMEOUT
|
|
154
|
+
hs = Sock.sphttp_tls_handshake_step(client)
|
|
155
|
+
while hs == 1 || hs == 2
|
|
156
|
+
remaining = deadline - Time.now.to_i
|
|
157
|
+
if remaining <= 0
|
|
158
|
+
return 0
|
|
159
|
+
end
|
|
160
|
+
mode = Tep::Scheduler::READ
|
|
161
|
+
if hs == 2
|
|
162
|
+
mode = Tep::Scheduler::WRITE
|
|
163
|
+
end
|
|
164
|
+
ready = Tep::Scheduler.io_wait(client, mode, remaining)
|
|
165
|
+
if ready == 0
|
|
166
|
+
return 0
|
|
167
|
+
end
|
|
168
|
+
hs = Sock.sphttp_tls_handshake_step(client)
|
|
169
|
+
end
|
|
170
|
+
if hs < 0
|
|
171
|
+
return 0
|
|
172
|
+
end
|
|
173
|
+
1
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Per-connection lifecycle.
|
|
177
|
+
def self.handle_connection(client)
|
|
178
|
+
# Inbound TLS: complete a non-blocking server handshake before
|
|
179
|
+
# reading anything. Runs inside this per-connection fiber so a
|
|
180
|
+
# slow handshake parks cooperatively instead of blocking the
|
|
181
|
+
# accept loop. On failure (incl. a plaintext client hitting the
|
|
182
|
+
# TLS port) drop the connection.
|
|
183
|
+
if Tep::APP.tls_cert.length > 0
|
|
184
|
+
if Tep::Server::Scheduled.tls_handshake(client) == 0
|
|
185
|
+
Sock.sphttp_close(client)
|
|
186
|
+
return 0
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
keep_going = true
|
|
190
|
+
while keep_going
|
|
191
|
+
blob = Tep::Server::Scheduled.read_request_blob(client, KEEPALIVE_TIMEOUT)
|
|
192
|
+
if blob.length == 0
|
|
193
|
+
break
|
|
194
|
+
end
|
|
195
|
+
req = Parser.parse(blob)
|
|
196
|
+
if req == nil
|
|
197
|
+
Tep::Server::Scheduled.send_simple(client, 400, "bad request")
|
|
198
|
+
break
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
req.consume_body_via_scheduler(client)
|
|
202
|
+
|
|
203
|
+
res = Response.new
|
|
204
|
+
Tep::APP.dispatch(req, res)
|
|
205
|
+
|
|
206
|
+
# Streaming responses use chunked Connection: close (same
|
|
207
|
+
# simplification as the prefork server) -- force the
|
|
208
|
+
# keep-alive loop to end after this response so the stream's
|
|
209
|
+
# terminator isn't followed by a stale read on the same fd.
|
|
210
|
+
keep_alive = req.keep_alive? && !res.halted_close? && !res.streaming
|
|
211
|
+
Tep::Server::Scheduled.write_response(client, req, res, keep_alive)
|
|
212
|
+
keep_going = keep_alive
|
|
213
|
+
end
|
|
214
|
+
Sock.sphttp_close(client)
|
|
215
|
+
0
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Non-blocking request reader. Returns the accumulated blob
|
|
219
|
+
# once "\r\n\r\n" is seen, or "" on timeout / EOF / oversize.
|
|
220
|
+
def self.read_request_blob(fd, timeout_seconds)
|
|
221
|
+
buf = ""
|
|
222
|
+
deadline = Time.now.to_i + timeout_seconds
|
|
223
|
+
while buf.length < MAX_REQUEST_BYTES
|
|
224
|
+
remaining = deadline - Time.now.to_i
|
|
225
|
+
if remaining <= 0
|
|
226
|
+
return ""
|
|
227
|
+
end
|
|
228
|
+
ready = Tep::Scheduler.io_wait(fd, Tep::Scheduler::READ, remaining)
|
|
229
|
+
if ready == 0
|
|
230
|
+
return ""
|
|
231
|
+
end
|
|
232
|
+
chunk = Sock.sphttp_recv_some(fd, 4096)
|
|
233
|
+
if chunk.length == 0
|
|
234
|
+
# Over TLS an empty read can be a partial record (SSL_read
|
|
235
|
+
# WANT_READ/WANT_WRITE), not EOF -- re-park on the indicated
|
|
236
|
+
# direction and retry rather than dropping the request. The
|
|
237
|
+
# loop top re-applies the deadline on the want-read path.
|
|
238
|
+
st = Sock.sphttp_io_status
|
|
239
|
+
if st == 1
|
|
240
|
+
next
|
|
241
|
+
end
|
|
242
|
+
if st == 2
|
|
243
|
+
Tep::Scheduler.io_wait(fd, Tep::Scheduler::WRITE, remaining)
|
|
244
|
+
next
|
|
245
|
+
end
|
|
246
|
+
return ""
|
|
247
|
+
end
|
|
248
|
+
buf = buf + chunk
|
|
249
|
+
if buf.length >= 4 && buf.include?("\r\n\r\n")
|
|
250
|
+
return buf
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
""
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Body-shape mirror of Tep::Server#write_response. Lifted into
|
|
257
|
+
# a cmeth so the connection fiber can call it without a captured
|
|
258
|
+
# `self`.
|
|
259
|
+
def self.write_response(client, req, res, keep_alive)
|
|
260
|
+
# WebSocket upgrade branch. Set by res.start_websocket in the
|
|
261
|
+
# user's handler after a successful Handshake.check. Writes
|
|
262
|
+
# the 101 Switching Protocols head, then assigns the client
|
|
263
|
+
# fd onto the driver and runs the recv loop. The recv loop
|
|
264
|
+
# returns when the connection closes (peer EOF, idle timeout,
|
|
265
|
+
# or a CLOSE frame round-trip). After return, the caller's
|
|
266
|
+
# handle_connection closes the fd as usual.
|
|
267
|
+
if res.upgrading_ws
|
|
268
|
+
head = Tep::WebSocket::Handshake.build_response(
|
|
269
|
+
res.ws_accept_key, res.ws_driver.subprotocol)
|
|
270
|
+
Sock.sphttp_write_str(client, head)
|
|
271
|
+
res.ws_driver.set_fd(client)
|
|
272
|
+
conn = Tep::WebSocket::Connection.new(res.ws_driver)
|
|
273
|
+
conn.run
|
|
274
|
+
return 0
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Streaming branch -- cooperative mirror of Tep::Server's
|
|
278
|
+
# streaming path (server.rb). Set by res.start_stream(streamer)
|
|
279
|
+
# in the handler. Writes a chunked-encoding head immediately,
|
|
280
|
+
# hands a Tep::Stream writer to the user's Streamer#pump, then
|
|
281
|
+
# emits the end-of-stream terminator. pump runs cooperatively:
|
|
282
|
+
# it parks on Tep::Scheduler.io_wait between writes (e.g. the
|
|
283
|
+
# proxy streamer waits on the upstream fd), so other fibers keep
|
|
284
|
+
# running while this stream is in flight. Connection: close --
|
|
285
|
+
# chunked keep-alive is legal but we keep it simple, matching
|
|
286
|
+
# the prefork server.
|
|
287
|
+
if res.streaming
|
|
288
|
+
res.headers["Transfer-Encoding"] = "chunked"
|
|
289
|
+
if !res.headers.key?("Content-Type")
|
|
290
|
+
res.headers["Content-Type"] = "text/event-stream"
|
|
291
|
+
end
|
|
292
|
+
reason = Tep.reason(res.status)
|
|
293
|
+
head = req.http_version + " " + res.status.to_s + " " + reason + "\r\n"
|
|
294
|
+
res.headers.each do |k, v|
|
|
295
|
+
head = head + k + ": " + v + "\r\n"
|
|
296
|
+
end
|
|
297
|
+
res.set_cookies.each do |line|
|
|
298
|
+
head = head + "Set-Cookie: " + line + "\r\n"
|
|
299
|
+
end
|
|
300
|
+
head = head + "Connection: close\r\n\r\n"
|
|
301
|
+
Sock.sphttp_write_str(client, head)
|
|
302
|
+
out = Tep::Stream.new(client)
|
|
303
|
+
res.streamer.pump(out)
|
|
304
|
+
Sock.sphttp_write_chunk_end(client)
|
|
305
|
+
return 0
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# File validators for cache revalidation (#152): a size-mtime
|
|
309
|
+
# ETag + Last-Modified, set before headers are serialized below.
|
|
310
|
+
if res.file_path.length > 0
|
|
311
|
+
fsz = Sock.sphttp_filesize(res.file_path)
|
|
312
|
+
fmt = Sock.sphttp_file_mtime(res.file_path)
|
|
313
|
+
if fsz >= 0 && fmt >= 0
|
|
314
|
+
res.etag(fsz.to_s + "-" + fmt.to_s)
|
|
315
|
+
res.last_modified(fmt)
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Conditional GET (issue #152): 304 + no body when the request's
|
|
320
|
+
# precondition matches the response's validator (ETag /
|
|
321
|
+
# Last-Modified, whether set by the handler or for a file above).
|
|
322
|
+
# For a file we also clear file_path so the sendfile branch below
|
|
323
|
+
# is skipped and the empty 304 goes out the inline-body path.
|
|
324
|
+
if Tep::Cache.not_modified?(req, res)
|
|
325
|
+
res.set_status(304)
|
|
326
|
+
res.set_body("")
|
|
327
|
+
res.file_path = ""
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Default Content-Type for inline-body responses. Matches
|
|
331
|
+
# Tep::Server#send; without it, the Security::Headers nosniff
|
|
332
|
+
# default leaves the browser refusing to interpret an erb
|
|
333
|
+
# response as HTML.
|
|
334
|
+
if res.file_path.length == 0 && res.body.length > 0 && !res.headers.key?("Content-Type")
|
|
335
|
+
res.headers["Content-Type"] = "text/html; charset=utf-8"
|
|
336
|
+
end
|
|
337
|
+
reason = Tep.reason(res.status)
|
|
338
|
+
head = req.http_version + " " + res.status.to_s + " " + reason + "\r\n"
|
|
339
|
+
res.headers.each do |k, v|
|
|
340
|
+
head = head + k + ": " + v + "\r\n"
|
|
341
|
+
end
|
|
342
|
+
res.set_cookies.each do |line|
|
|
343
|
+
head = head + "Set-Cookie: " + line + "\r\n"
|
|
344
|
+
end
|
|
345
|
+
if keep_alive
|
|
346
|
+
head = head + "Connection: keep-alive\r\n"
|
|
347
|
+
else
|
|
348
|
+
head = head + "Connection: close\r\n"
|
|
349
|
+
end
|
|
350
|
+
if res.file_path.length > 0
|
|
351
|
+
fs = Sock.sphttp_filesize(res.file_path)
|
|
352
|
+
head = head + "Content-Length: " + fs.to_s + "\r\n\r\n"
|
|
353
|
+
Sock.sphttp_write_str(client, head)
|
|
354
|
+
Sock.sphttp_sendfile(client, res.file_path)
|
|
355
|
+
else
|
|
356
|
+
head = head + "Content-Length: " + res.body.length.to_s + "\r\n\r\n"
|
|
357
|
+
Sock.sphttp_write_str(client, head)
|
|
358
|
+
if res.body.length > 0
|
|
359
|
+
Sock.sphttp_write_str(client, res.body)
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
0
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def self.send_simple(client, status, msg)
|
|
366
|
+
reason = Tep.reason(status)
|
|
367
|
+
head = "HTTP/1.0 " + status.to_s + " " + reason + "\r\n" +
|
|
368
|
+
"Content-Length: " + msg.length.to_s + "\r\n" +
|
|
369
|
+
"Connection: close\r\n\r\n" + msg
|
|
370
|
+
Sock.sphttp_write_str(client, head)
|
|
371
|
+
0
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|