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/request.rb
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Tep::Request -- what the handler reads off the wire.
|
|
2
|
+
module Tep
|
|
3
|
+
class Request
|
|
4
|
+
attr_accessor :verb, :path, :raw_path, :http_version
|
|
5
|
+
attr_accessor :params, :query, :req_headers, :raw_body, :cookies, :session
|
|
6
|
+
attr_accessor :remote_host
|
|
7
|
+
attr_accessor :ivars
|
|
8
|
+
# Set by the auth-filter (Tep::AuthFilter, run before the user's
|
|
9
|
+
# before-filter -- see Tep::App#auth_filter). Always populated:
|
|
10
|
+
# Tep::Identity.anonymous when no provider matched, otherwise
|
|
11
|
+
# the matched provider's Identity. Handlers and filters can
|
|
12
|
+
# rely on req.identity being non-nil.
|
|
13
|
+
attr_accessor :identity
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@verb = ""
|
|
17
|
+
@path = ""
|
|
18
|
+
@raw_path = ""
|
|
19
|
+
@http_version = "HTTP/1.0"
|
|
20
|
+
@params = Tep.str_hash # path captures + query + form merged
|
|
21
|
+
@query = Tep.str_hash # raw query string only
|
|
22
|
+
@req_headers = Tep.str_hash # downcased header names; renamed
|
|
23
|
+
# from `headers` to avoid sharing
|
|
24
|
+
# an ivar slot with Response (spinel
|
|
25
|
+
# mis-codegens polymorphic ivar
|
|
26
|
+
# writes when two classes share an
|
|
27
|
+
# ivar name).
|
|
28
|
+
@cookies = Tep.str_hash # parsed from Cookie: header
|
|
29
|
+
@session = Session.new # signed cookie store
|
|
30
|
+
@raw_body = "" # same reasoning as req_headers
|
|
31
|
+
@remote_host = ""
|
|
32
|
+
@passed = false # `pass` flag: skip to the next matching route
|
|
33
|
+
@ivars = Tep.str_hash # per-request bag for `@name = ...`
|
|
34
|
+
# set by handlers and `before` filters,
|
|
35
|
+
# read by templates as `ivars[k]`. The
|
|
36
|
+
# Sinatra-compat translator rewrites
|
|
37
|
+
# `@x = v` -> `req.ivars["x"] = (v).to_s`
|
|
38
|
+
# in handler bodies and `@x` -> `ivars["x"]`
|
|
39
|
+
# inside ERB chunks.
|
|
40
|
+
@identity = Tep::Identity.anonymous
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
attr_accessor :passed
|
|
44
|
+
def set_passed; @passed = true; end
|
|
45
|
+
|
|
46
|
+
# Sinatra-compat read aliases. Writers stay on the renamed slots
|
|
47
|
+
# (req_headers, raw_body) -- a `req.headers["X"] = v` from user
|
|
48
|
+
# code goes through these getters, but assignment back into the
|
|
49
|
+
# request via this method name is intentionally not provided
|
|
50
|
+
# (the framework doesn't expect handlers to mutate the request).
|
|
51
|
+
def headers; @req_headers; end
|
|
52
|
+
def body; @raw_body; end
|
|
53
|
+
|
|
54
|
+
# Spinel's Hash[k] returns "" for missing string keys, not nil --
|
|
55
|
+
# so an empty Connection header looks the same as no header at all.
|
|
56
|
+
# We treat both as "use HTTP/1.1 default behaviour".
|
|
57
|
+
def keep_alive?
|
|
58
|
+
lc = @req_headers["connection"].downcase
|
|
59
|
+
if lc == "close"
|
|
60
|
+
return false
|
|
61
|
+
end
|
|
62
|
+
if lc == "keep-alive"
|
|
63
|
+
return true
|
|
64
|
+
end
|
|
65
|
+
@http_version == "HTTP/1.1"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def content_length
|
|
69
|
+
@req_headers["content-length"].to_i
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def form?
|
|
73
|
+
@req_headers["content-type"].downcase.start_with?("application/x-www-form-urlencoded")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# True when the request body is a multipart/form-data submission
|
|
77
|
+
# (browsers use this for any form built via `new FormData(...)`
|
|
78
|
+
# or carrying file inputs). Tep::Multipart.parse handles the
|
|
79
|
+
# text fields; file-upload parts are skipped in v1.
|
|
80
|
+
def multipart?
|
|
81
|
+
@req_headers["content-type"].downcase.start_with?("multipart/form-data")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# ---- Rack::Request-style accessors (reads only, no .ip yet) ----
|
|
85
|
+
# These are convenience getters over headers we already parse;
|
|
86
|
+
# `.ip` would need a sphttp_accept_with_peer C helper before it
|
|
87
|
+
# can land cleanly, so it's deferred.
|
|
88
|
+
|
|
89
|
+
def host; @req_headers["host"]; end
|
|
90
|
+
def user_agent; @req_headers["user-agent"]; end
|
|
91
|
+
def referer; @req_headers["referer"]; end
|
|
92
|
+
def referrer; @req_headers["referer"]; end # spelling alias
|
|
93
|
+
def accept; @req_headers["accept"]; end
|
|
94
|
+
def content_type; @req_headers["content-type"]; end
|
|
95
|
+
|
|
96
|
+
# tep doesn't terminate TLS itself; both flags reflect "is this
|
|
97
|
+
# connection encrypted from the client's view?" via the
|
|
98
|
+
# `X-Forwarded-Proto: https` header that any reasonable reverse
|
|
99
|
+
# proxy sets.
|
|
100
|
+
def scheme
|
|
101
|
+
proto = @req_headers["x-forwarded-proto"]
|
|
102
|
+
if proto.length > 0
|
|
103
|
+
return proto.downcase
|
|
104
|
+
end
|
|
105
|
+
"http"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def ssl?
|
|
109
|
+
scheme == "https"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Pull any remaining body bytes from `client_fd` up to the
|
|
113
|
+
# advertised Content-Length, then merge form / multipart fields
|
|
114
|
+
# into @params. Used by Tep::Server (prefork, blocking fds) --
|
|
115
|
+
# under the prefork model recv() blocks naturally until bytes
|
|
116
|
+
# arrive, so `sphttp_drain_body` (a tight blocking-recv loop)
|
|
117
|
+
# is the right primitive.
|
|
118
|
+
#
|
|
119
|
+
# Tep::Server::Scheduled uses `consume_body_via_scheduler` below
|
|
120
|
+
# instead, because its client fd is non-blocking + a blocking
|
|
121
|
+
# recv would starve the whole worker.
|
|
122
|
+
#
|
|
123
|
+
# No-op on bodyless requests. Form parsing handles
|
|
124
|
+
# `application/x-www-form-urlencoded`; multipart handles
|
|
125
|
+
# `multipart/form-data` (text fields only; file uploads skipped).
|
|
126
|
+
# Other content types leave @raw_body intact for handlers that
|
|
127
|
+
# want to consume it directly.
|
|
128
|
+
def consume_body(client_fd)
|
|
129
|
+
cl = content_length
|
|
130
|
+
already = @raw_body.length
|
|
131
|
+
if cl > already
|
|
132
|
+
rest = Sock.sphttp_drain_body(client_fd, cl - already)
|
|
133
|
+
@raw_body = @raw_body + rest
|
|
134
|
+
end
|
|
135
|
+
parse_form_body
|
|
136
|
+
0
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Scheduler-friendly body drain. Loops on
|
|
140
|
+
# `Sock.sphttp_recv_some` + `Tep::Scheduler.io_wait` so other
|
|
141
|
+
# fibers keep running while we wait for body bytes. Per-recv
|
|
142
|
+
# timeout caps the wait at 5s -- a client that opened the
|
|
143
|
+
# request but never sent the body gets dropped instead of
|
|
144
|
+
# hanging the fiber forever.
|
|
145
|
+
#
|
|
146
|
+
# Returns @raw_body.length after the drain. Body parsing
|
|
147
|
+
# (form / multipart -> @params) happens at the end via
|
|
148
|
+
# parse_form_body, same shape as consume_body.
|
|
149
|
+
def consume_body_via_scheduler(client_fd)
|
|
150
|
+
cl = content_length
|
|
151
|
+
while @raw_body.length < cl
|
|
152
|
+
ready = Tep::Scheduler.io_wait(client_fd, Tep::Scheduler::READ, 5)
|
|
153
|
+
if ready == 0
|
|
154
|
+
break # timeout -- client never finished sending
|
|
155
|
+
end
|
|
156
|
+
chunk = Sock.sphttp_recv_some(client_fd, cl - @raw_body.length)
|
|
157
|
+
if chunk.length == 0
|
|
158
|
+
# Over TLS an empty read can be a partial record (SSL_read
|
|
159
|
+
# WANT_READ/WANT_WRITE) rather than a peer close -- re-park on
|
|
160
|
+
# the indicated direction and retry instead of truncating the
|
|
161
|
+
# body. Plaintext EOF/error (status 3/-1) still breaks.
|
|
162
|
+
st = Sock.sphttp_io_status
|
|
163
|
+
if st == 1
|
|
164
|
+
next
|
|
165
|
+
end
|
|
166
|
+
if st == 2
|
|
167
|
+
Tep::Scheduler.io_wait(client_fd, Tep::Scheduler::WRITE, 5)
|
|
168
|
+
next
|
|
169
|
+
end
|
|
170
|
+
break # peer closed mid-body
|
|
171
|
+
end
|
|
172
|
+
@raw_body = @raw_body + chunk
|
|
173
|
+
end
|
|
174
|
+
parse_form_body
|
|
175
|
+
0
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Shared form / multipart -> @params merge. Both server-side
|
|
179
|
+
# body-drain paths call this once their drain step has filled
|
|
180
|
+
# @raw_body to Content-Length.
|
|
181
|
+
def parse_form_body
|
|
182
|
+
if form?
|
|
183
|
+
Url.parse_query(@raw_body).each do |k, v|
|
|
184
|
+
@params[k] = v
|
|
185
|
+
end
|
|
186
|
+
elsif multipart?
|
|
187
|
+
Tep::Multipart.parse(@raw_body, @req_headers["content-type"]).each do |k, v|
|
|
188
|
+
@params[k] = v
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
0
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
data/lib/tep/response.rb
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Tep::Response -- what the handler writes back. Headers are a Bag
|
|
2
|
+
# (string-keyed); the framework adds Content-Length / Connection
|
|
3
|
+
# automatically when serializing.
|
|
4
|
+
module Tep
|
|
5
|
+
class Response
|
|
6
|
+
attr_accessor :status, :headers, :body, :halted, :file_path, :set_cookies
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@status = 200
|
|
10
|
+
@headers = Tep.str_hash
|
|
11
|
+
@body = ""
|
|
12
|
+
@halted = false
|
|
13
|
+
@file_path = ""
|
|
14
|
+
# `Set-Cookie` is a header that can repeat; can't shove multiple
|
|
15
|
+
# values into a Hash slot. Each entry here is one fully-formatted
|
|
16
|
+
# Set-Cookie line, emitted verbatim by the writer.
|
|
17
|
+
@set_cookies = [""]
|
|
18
|
+
@set_cookies.delete_at(0)
|
|
19
|
+
@streamer = Streamer.new # default no-op; only used when @streaming
|
|
20
|
+
@streaming = false
|
|
21
|
+
# WebSocket upgrade slots. The Tep::Server::Scheduled write
|
|
22
|
+
# path sees @upgrading_ws and, instead of writing the normal
|
|
23
|
+
# status-line response body, emits the 101 handshake response
|
|
24
|
+
# then drives the recv loop via Tep::WebSocket::Connection
|
|
25
|
+
# until the connection closes.
|
|
26
|
+
@upgrading_ws = false
|
|
27
|
+
@ws_accept_key = ""
|
|
28
|
+
@ws_driver = Tep::WebSocket::Driver.new(0)
|
|
29
|
+
# Last-Modified validator as epoch seconds (0 = unset). The header
|
|
30
|
+
# carries the formatted date; this is kept for the conditional-GET
|
|
31
|
+
# comparison against If-Modified-Since (issue #152).
|
|
32
|
+
@lastmod_epoch = 0
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
attr_accessor :streamer, :streaming
|
|
36
|
+
attr_accessor :upgrading_ws, :ws_accept_key, :ws_driver
|
|
37
|
+
attr_reader :lastmod_epoch
|
|
38
|
+
|
|
39
|
+
# ---- HTTP caching helpers (issue #152) ----
|
|
40
|
+
|
|
41
|
+
# Set the Cache-Control header verbatim.
|
|
42
|
+
def cache_control(v)
|
|
43
|
+
@headers["Cache-Control"] = v
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Common Cache-Control shortcuts.
|
|
48
|
+
def no_store; cache_control("no-store"); end
|
|
49
|
+
def no_cache; cache_control("no-cache"); end
|
|
50
|
+
|
|
51
|
+
# Cacheable for `secs` seconds: set both Expires (absolute HTTP-date)
|
|
52
|
+
# and Cache-Control: max-age (relative).
|
|
53
|
+
def expires(secs)
|
|
54
|
+
@headers["Expires"] = Sock.sphttp_http_date(Time.now.to_i + secs)
|
|
55
|
+
@headers["Cache-Control"] = "max-age=" + secs.to_s
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Strong ETag validator (quoted per RFC 7232).
|
|
60
|
+
def etag(value)
|
|
61
|
+
@headers["ETag"] = "\"" + value + "\""
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Last-Modified validator from Unix epoch seconds. Remembers the
|
|
66
|
+
# epoch so conditional GET can compare it to If-Modified-Since.
|
|
67
|
+
def last_modified(epoch)
|
|
68
|
+
@lastmod_epoch = epoch
|
|
69
|
+
@headers["Last-Modified"] = Sock.sphttp_http_date(epoch)
|
|
70
|
+
self
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def start_stream(streamer)
|
|
74
|
+
@streamer = streamer
|
|
75
|
+
@streaming = true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Mark the response as a WebSocket upgrade. The server writes a
|
|
79
|
+
# 101 Switching Protocols response with the accept-key, assigns
|
|
80
|
+
# the live client fd onto the driver, then runs the recv loop.
|
|
81
|
+
def start_websocket(accept_key, driver)
|
|
82
|
+
@upgrading_ws = true
|
|
83
|
+
@ws_accept_key = accept_key
|
|
84
|
+
@ws_driver = driver
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Sinatra-style cookie writer. `opts` is a Bag-of-strings
|
|
88
|
+
# (path, expires, max-age, domain, samesite, httponly, secure).
|
|
89
|
+
# Empty `opts` is fine: just writes "name=value".
|
|
90
|
+
def set_cookie(name, value, opts)
|
|
91
|
+
line = name + "=" + Url.escape(value)
|
|
92
|
+
if opts.length > 0
|
|
93
|
+
opts.each do |k, v|
|
|
94
|
+
if v.length == 0
|
|
95
|
+
line = line + "; " + k # bare flag (HttpOnly, Secure)
|
|
96
|
+
else
|
|
97
|
+
line = line + "; " + k + "=" + v
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
@set_cookies.push(line)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def send_file(path)
|
|
105
|
+
@file_path = path
|
|
106
|
+
@body = ""
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Spinel's polymorphic-receiver write codegen emits a no-op for
|
|
110
|
+
# `res.body = x` when called from a context that has a poly
|
|
111
|
+
# value, so we force the assignment through this method (where
|
|
112
|
+
# `self` is unambiguously Response).
|
|
113
|
+
def set_body_if_empty(s)
|
|
114
|
+
if @body.length == 0 && s.length > 0
|
|
115
|
+
@body = s
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Unconditional body setter. Same poly-write rationale as
|
|
120
|
+
# set_body_if_empty (self is unambiguously Response here, so the
|
|
121
|
+
# `@body = s` codegens correctly), but always assigns -- used by
|
|
122
|
+
# Tep::Proxy, which writes the upstream body whether or not it's
|
|
123
|
+
# empty (a 204 / empty upstream body must overwrite, not skip).
|
|
124
|
+
def set_body(s)
|
|
125
|
+
@body = s
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def set_status(n); @status = n; end
|
|
129
|
+
|
|
130
|
+
def halted_close?
|
|
131
|
+
@halted && @status >= 300
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
data/lib/tep/router.rb
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Matches incoming requests against a static Route table built up
|
|
2
|
+
# by the Tep.<verb> DSL. Path patterns: literal segments + ":name"
|
|
3
|
+
# captures + "*" splat, OR a regex (via the handler's `is_regex?`).
|
|
4
|
+
#
|
|
5
|
+
# Spinel's type inference unifies parameters across classes that
|
|
6
|
+
# share an ivar name. So Route uses `r_verb` / `r_pat` rather than
|
|
7
|
+
# the more readable `verb` / `pattern` -- otherwise `req.verb` and
|
|
8
|
+
# `route.verb` would make `req` and `route` indistinguishable to
|
|
9
|
+
# the codegen and break ivar writes downstream.
|
|
10
|
+
module Tep
|
|
11
|
+
class Route
|
|
12
|
+
attr_accessor :r_verb, :r_pat, :r_handler, :r_params
|
|
13
|
+
|
|
14
|
+
def initialize(verb, pattern, handler)
|
|
15
|
+
@r_verb = verb
|
|
16
|
+
@r_pat = pattern
|
|
17
|
+
@r_handler = handler
|
|
18
|
+
@r_params = []
|
|
19
|
+
pattern.split("/").each do |part|
|
|
20
|
+
if part.length > 0 && part[0] == ":"
|
|
21
|
+
@r_params.push(part[1, part.length - 1])
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def handler; @r_handler; end
|
|
27
|
+
|
|
28
|
+
def matches?(req_verb, req_path)
|
|
29
|
+
if req_verb != @r_verb
|
|
30
|
+
return false
|
|
31
|
+
end
|
|
32
|
+
if @r_handler.is_regex?
|
|
33
|
+
return @r_handler.re_match?(req_path)
|
|
34
|
+
end
|
|
35
|
+
pat = @r_pat.split("/")
|
|
36
|
+
req = req_path.split("/")
|
|
37
|
+
if pat.length != req.length
|
|
38
|
+
return false
|
|
39
|
+
end
|
|
40
|
+
i = 0
|
|
41
|
+
while i < pat.length
|
|
42
|
+
pp = pat[i]
|
|
43
|
+
rp = req[i]
|
|
44
|
+
if pp.length > 0 && pp[0] == ":"
|
|
45
|
+
if rp.length == 0
|
|
46
|
+
return false
|
|
47
|
+
end
|
|
48
|
+
elsif pp == "*"
|
|
49
|
+
if rp.length == 0
|
|
50
|
+
return false
|
|
51
|
+
end
|
|
52
|
+
else
|
|
53
|
+
if pp != rp
|
|
54
|
+
return false
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
i += 1
|
|
58
|
+
end
|
|
59
|
+
true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def fold_captures(req)
|
|
63
|
+
if @r_handler.is_regex?
|
|
64
|
+
caps = @r_handler.re_capture(req.path)
|
|
65
|
+
i = 0
|
|
66
|
+
while i < caps.length
|
|
67
|
+
req.params[(i + 1).to_s] = caps[i]
|
|
68
|
+
i += 1
|
|
69
|
+
end
|
|
70
|
+
return
|
|
71
|
+
end
|
|
72
|
+
pat = @r_pat.split("/")
|
|
73
|
+
rp = req.path.split("/")
|
|
74
|
+
pi = 0
|
|
75
|
+
i = 0
|
|
76
|
+
while i < pat.length
|
|
77
|
+
pp = pat[i]
|
|
78
|
+
if pp.length > 0 && pp[0] == ":"
|
|
79
|
+
name = @r_params[pi]
|
|
80
|
+
req.params[name] = Url.unescape(rp[i])
|
|
81
|
+
pi += 1
|
|
82
|
+
end
|
|
83
|
+
i += 1
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
class Router
|
|
89
|
+
attr_accessor :routes
|
|
90
|
+
|
|
91
|
+
def initialize
|
|
92
|
+
@routes = [Route.new("", "", Handler.new)] # type-seed sentinel
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def add(verb, pattern, handler)
|
|
96
|
+
@routes.push(Route.new(verb, pattern, handler))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def match(req)
|
|
100
|
+
i = 1 # skip the seed at index 0
|
|
101
|
+
while i < @routes.length
|
|
102
|
+
r = @routes[i]
|
|
103
|
+
if r.matches?(req.verb, req.path)
|
|
104
|
+
return r
|
|
105
|
+
end
|
|
106
|
+
i += 1
|
|
107
|
+
end
|
|
108
|
+
nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Find the next matching route after `start_idx` (1-based; the
|
|
112
|
+
# seed at 0 is skipped). Used by `pass` to step to the next
|
|
113
|
+
# candidate. Returns the Route + its index, or nil + -1.
|
|
114
|
+
def match_after(req, start_idx)
|
|
115
|
+
i = start_idx + 1
|
|
116
|
+
while i < @routes.length
|
|
117
|
+
r = @routes[i]
|
|
118
|
+
if r.matches?(req.verb, req.path)
|
|
119
|
+
return r
|
|
120
|
+
end
|
|
121
|
+
i += 1
|
|
122
|
+
end
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def index_of(route)
|
|
127
|
+
i = 0
|
|
128
|
+
while i < @routes.length
|
|
129
|
+
if @routes[i] == route
|
|
130
|
+
return i
|
|
131
|
+
end
|
|
132
|
+
i += 1
|
|
133
|
+
end
|
|
134
|
+
-1
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|