homura-runtime 0.1.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/CHANGELOG.md +6 -0
- data/README.md +37 -0
- data/bin/cloudflare-workers-build +255 -0
- data/docs/ARCHITECTURE.md +59 -0
- data/exe/auto-await +102 -0
- data/exe/compile-assets +142 -0
- data/exe/compile-erb +262 -0
- data/lib/cloudflare_workers/ai.rb +197 -0
- data/lib/cloudflare_workers/async_registry.rb +198 -0
- data/lib/cloudflare_workers/auto_await/analyzer.rb +264 -0
- data/lib/cloudflare_workers/auto_await/transformer.rb +19 -0
- data/lib/cloudflare_workers/cache.rb +234 -0
- data/lib/cloudflare_workers/durable_object.rb +590 -0
- data/lib/cloudflare_workers/email.rb +180 -0
- data/lib/cloudflare_workers/http.rb +164 -0
- data/lib/cloudflare_workers/multipart.rb +332 -0
- data/lib/cloudflare_workers/queue.rb +407 -0
- data/lib/cloudflare_workers/scheduled.rb +185 -0
- data/lib/cloudflare_workers/stream.rb +317 -0
- data/lib/cloudflare_workers/version.rb +5 -0
- data/lib/cloudflare_workers.rb +801 -0
- data/lib/opal_patches.rb +653 -0
- data/runtime/patch-opal-evals.mjs +66 -0
- data/runtime/setup-node-crypto.mjs +20 -0
- data/runtime/worker.mjs +9 -0
- data/runtime/worker_module.mjs +384 -0
- data/runtime/wrangler.toml.example +40 -0
- data/templates/wrangler.toml.example +8 -0
- metadata +104 -0
data/lib/opal_patches.rb
ADDED
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
# backtick_javascript: true
|
|
2
|
+
# Runtime patches to extend Opal's corelib with methods required by
|
|
3
|
+
# real-world gems (Sinatra, Rack, Mustermann, ...) that are missing
|
|
4
|
+
# from upstream opal 1.8.3.rc1. Each patch is kept strictly additive:
|
|
5
|
+
# it only installs a method if the method does not already exist.
|
|
6
|
+
#
|
|
7
|
+
# We prefer to patch Opal here (in the adapter layer) rather than
|
|
8
|
+
# modifying the vendored gems, so that vendored code keeps the same
|
|
9
|
+
# shape as its upstream counterparts. If a patch here turns out to
|
|
10
|
+
# fix something upstream Opal is missing, it should be turned into
|
|
11
|
+
# a PR against github.com/opal/opal.
|
|
12
|
+
|
|
13
|
+
# -----------------------------------------------------------------
|
|
14
|
+
# Module#deprecate_constant
|
|
15
|
+
# -----------------------------------------------------------------
|
|
16
|
+
# CRuby 2.6+ ships `Module#deprecate_constant(*names)` which marks the
|
|
17
|
+
# listed constants so that accessing them prints a deprecation warning.
|
|
18
|
+
# Opal 1.8.3.rc1 does not implement this, so any gem that calls
|
|
19
|
+
# `deprecate_constant :FOO` at module-load time aborts with a
|
|
20
|
+
# method_missing error. Real-world examples that hit this in Phase 2:
|
|
21
|
+
# * rack/multipart/parser.rb -> deprecate_constant :CHARSET
|
|
22
|
+
#
|
|
23
|
+
# Ruby's behaviour is "warn on read, still return the value", which we
|
|
24
|
+
# approximate here as a no-op (Opal has no const-access hook to warn
|
|
25
|
+
# from, and a warning-only behaviour does not affect program output).
|
|
26
|
+
class Module
|
|
27
|
+
unless private_method_defined?(:deprecate_constant) || method_defined?(:deprecate_constant)
|
|
28
|
+
def deprecate_constant(*_names)
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
private :deprecate_constant
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# -----------------------------------------------------------------
|
|
36
|
+
# Encoding::* constants that Opal corelib does not register
|
|
37
|
+
# -----------------------------------------------------------------
|
|
38
|
+
# Opal ships a handful of encodings in opal/corelib/string/encoding.rb
|
|
39
|
+
# (UTF-8, UTF-16{LE,BE}, UTF-32{LE,BE}, ASCII-8BIT, ISO-8859-1, US-ASCII).
|
|
40
|
+
# Real-world gems reference many more legacy encodings that Opal never
|
|
41
|
+
# declares. When such a constant appears at class-load time (e.g. in a
|
|
42
|
+
# constant hash literal inside rack/multipart/parser.rb), Opal raises
|
|
43
|
+
# `NameError: uninitialized constant Encoding::FOO` and aborts the
|
|
44
|
+
# whole require chain.
|
|
45
|
+
#
|
|
46
|
+
# Real transcoding is out of scope for Phase 2 — Workers do not need to
|
|
47
|
+
# convert ISO-2022-JP for hello-world. We install each missing constant
|
|
48
|
+
# as an alias of an encoding Opal already ships so that the constant
|
|
49
|
+
# reference succeeds. If a gem actually calls .encode onto one of these
|
|
50
|
+
# Opal will raise a clear error at the call site, which is what we want.
|
|
51
|
+
# -----------------------------------------------------------------
|
|
52
|
+
# Module#const_defined? / Module#const_get — qualified name support
|
|
53
|
+
# -----------------------------------------------------------------
|
|
54
|
+
# CRuby's `Module#const_defined?` and `Module#const_get` both accept
|
|
55
|
+
# "Foo::Bar::Baz" style qualified names. Opal 1.8.3.rc1 supports
|
|
56
|
+
# qualified names in `const_get` but NOT in `const_defined?`, so any
|
|
57
|
+
# call like `Object.const_defined?('Mustermann::AST::Node::Root')`
|
|
58
|
+
# returns false (or raises NameError) even when the constant exists,
|
|
59
|
+
# and Mustermann's `Node[:root]` factory falls through to `nil`.
|
|
60
|
+
#
|
|
61
|
+
# Patch: split qualified names in `const_defined?` and walk the chain
|
|
62
|
+
# exactly like `const_get` does.
|
|
63
|
+
class Module
|
|
64
|
+
unless instance_method(:const_defined?).source_location.nil?
|
|
65
|
+
# pass — we monkey-patch regardless
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
alias_method :__homura_const_defined_simple, :const_defined?
|
|
69
|
+
|
|
70
|
+
def const_defined?(name, inherit = true)
|
|
71
|
+
name_str = name.to_s
|
|
72
|
+
if name_str.include?('::')
|
|
73
|
+
parts = name_str.split('::')
|
|
74
|
+
parts.shift if parts.first.empty? # leading "::Foo::Bar"
|
|
75
|
+
current = self
|
|
76
|
+
parts.each do |part|
|
|
77
|
+
return false unless current.__homura_const_defined_simple(part, inherit)
|
|
78
|
+
current = current.const_get(part, inherit)
|
|
79
|
+
return false unless current.is_a?(Module)
|
|
80
|
+
end
|
|
81
|
+
true
|
|
82
|
+
else
|
|
83
|
+
__homura_const_defined_simple(name, inherit)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# -----------------------------------------------------------------
|
|
89
|
+
# Global defaults that Opal does not initialise
|
|
90
|
+
# -----------------------------------------------------------------
|
|
91
|
+
# CRuby sets `$0` to the program name from argv[0]. Opal leaves it
|
|
92
|
+
# as nil, which breaks gems that call `File.expand_path($0)` at class-
|
|
93
|
+
# body time (sinatra/main.rb: `proc { File.expand_path($0) }`).
|
|
94
|
+
# Install a harmless default string.
|
|
95
|
+
$0 ||= '(homura)'
|
|
96
|
+
$PROGRAM_NAME ||= $0
|
|
97
|
+
|
|
98
|
+
# (Previously this file force-set APP_ENV/RACK_ENV to 'production' to
|
|
99
|
+
# keep Rack::ShowExceptions out of the way — its ERB renderer uses
|
|
100
|
+
# `binding.eval` which lands on `new Function($code)`, forbidden on
|
|
101
|
+
# Workers. The real fix is now in `vendor/rack/show_exceptions.rb`
|
|
102
|
+
# where `#pretty` builds the HTML directly, so we no longer need to
|
|
103
|
+
# hide development mode. Users who want production settings should
|
|
104
|
+
# set `APP_ENV=production` themselves, same as on any Rack server.)
|
|
105
|
+
|
|
106
|
+
# Load Opal's stdlib Forwardable BEFORE patching it, so our overrides
|
|
107
|
+
# are applied last and are not clobbered when a vendored gem requires
|
|
108
|
+
# 'forwardable' transitively.
|
|
109
|
+
require 'forwardable'
|
|
110
|
+
|
|
111
|
+
# -----------------------------------------------------------------
|
|
112
|
+
# (removed) Debug method_missing logger — was used while iterating
|
|
113
|
+
# through Phase 2 init-time issues. Permanent fix for the root cause
|
|
114
|
+
# (Opal `@prototype` collision) landed in vendor/opal-gem/. Keeping
|
|
115
|
+
# method_missing unpatched restores the fast path for real requests.
|
|
116
|
+
# -----------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
# -----------------------------------------------------------------
|
|
119
|
+
# Forwardable#def_instance_delegator — support for expression accessors
|
|
120
|
+
# -----------------------------------------------------------------
|
|
121
|
+
# CRuby's Forwardable evaluates the `accessor` argument as a Ruby
|
|
122
|
+
# expression when it is neither an ivar (`@foo`) nor a plain method
|
|
123
|
+
# name — so `def_delegator 'self.class', :foo` delegates to the current
|
|
124
|
+
# class's #foo method. Opal's simplified Forwardable (stdlib/forwardable.rb)
|
|
125
|
+
# just treats the accessor literally and calls `__send__('self.class')`,
|
|
126
|
+
# which raises method_missing.
|
|
127
|
+
#
|
|
128
|
+
# Mustermann's AST::Pattern relies on this exact CRuby behaviour:
|
|
129
|
+
#
|
|
130
|
+
# instance_delegate %i[parser compiler ...] => 'self.class'
|
|
131
|
+
#
|
|
132
|
+
# so without this patch the first Mustermann::Pattern#compile call from
|
|
133
|
+
# Sinatra::Base#route aborts the whole require chain. The fix below
|
|
134
|
+
# re-implements def_instance_delegator / def_single_delegator so that
|
|
135
|
+
# non-ivar accessors that look like Ruby expressions go through
|
|
136
|
+
# `instance_eval` / `class_eval` instead of `__send__`.
|
|
137
|
+
# homura patch: Cloudflare Workers disallows `new Function($code)` /
|
|
138
|
+
# `eval($code)` (Workers' "Code generation from strings disallowed" rule).
|
|
139
|
+
# Opal's `instance_eval(String)` compiles to a `new Function($code)` call,
|
|
140
|
+
# so any Forwardable delegation with an *expression* accessor (Mustermann's
|
|
141
|
+
# `instance_delegate %i[parser compiler] => 'self.class'` is the real-world
|
|
142
|
+
# trigger) crashes on Workers at first dispatch.
|
|
143
|
+
#
|
|
144
|
+
# The helper below walks a small subset of dot-separated accessor
|
|
145
|
+
# expressions — `self`, `self.class`, `@ivar`, `@ivar.method`, plain
|
|
146
|
+
# `method_name`, `method_name.other_method` — without going through
|
|
147
|
+
# `instance_eval`. Mustermann (and the Ruby stdlib itself) only uses
|
|
148
|
+
# identifiers from that subset, so we never need the full Ruby parser.
|
|
149
|
+
module ForwardableAccessor
|
|
150
|
+
module_function
|
|
151
|
+
|
|
152
|
+
def resolve(instance, expr)
|
|
153
|
+
expr = expr.to_s
|
|
154
|
+
current = instance
|
|
155
|
+
expr.split('.').each do |part|
|
|
156
|
+
current = if part == 'self'
|
|
157
|
+
instance
|
|
158
|
+
elsif part.start_with?('@')
|
|
159
|
+
instance.instance_variable_get(part)
|
|
160
|
+
else
|
|
161
|
+
current.__send__(part)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
current
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
module Forwardable
|
|
169
|
+
remove_method :def_instance_delegator if method_defined?(:def_instance_delegator)
|
|
170
|
+
|
|
171
|
+
def def_instance_delegator(accessor, method, ali = method)
|
|
172
|
+
accessor_str = accessor.to_s
|
|
173
|
+
if accessor_str.start_with?('@') && !accessor_str.include?('.')
|
|
174
|
+
define_method ali do |*args, &block|
|
|
175
|
+
instance_variable_get(accessor_str).__send__(method, *args, &block)
|
|
176
|
+
end
|
|
177
|
+
elsif accessor_str =~ /\A[A-Za-z_]\w*\z/
|
|
178
|
+
# Plain identifier (method name) — call via __send__ as before.
|
|
179
|
+
define_method ali do |*args, &block|
|
|
180
|
+
__send__(accessor_str).__send__(method, *args, &block)
|
|
181
|
+
end
|
|
182
|
+
else
|
|
183
|
+
# Dot-path expression like 'self.class'. Resolve without eval.
|
|
184
|
+
define_method ali do |*args, &block|
|
|
185
|
+
ForwardableAccessor.resolve(self, accessor_str).__send__(method, *args, &block)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
module SingleForwardable
|
|
192
|
+
remove_method :def_single_delegator if method_defined?(:def_single_delegator)
|
|
193
|
+
|
|
194
|
+
def def_single_delegator(accessor, method, ali = method)
|
|
195
|
+
accessor_str = accessor.to_s
|
|
196
|
+
if accessor_str.start_with?('@') && !accessor_str.include?('.')
|
|
197
|
+
define_singleton_method ali do |*args, &block|
|
|
198
|
+
instance_variable_get(accessor_str).__send__(method, *args, &block)
|
|
199
|
+
end
|
|
200
|
+
elsif accessor_str =~ /\A[A-Za-z_]\w*\z/
|
|
201
|
+
define_singleton_method ali do |*args, &block|
|
|
202
|
+
__send__(accessor_str).__send__(method, *args, &block)
|
|
203
|
+
end
|
|
204
|
+
else
|
|
205
|
+
define_singleton_method ali do |*args, &block|
|
|
206
|
+
ForwardableAccessor.resolve(self, accessor_str).__send__(method, *args, &block)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# -----------------------------------------------------------------
|
|
213
|
+
# URI::DEFAULT_PARSER — CGI-backed stand-in
|
|
214
|
+
# -----------------------------------------------------------------
|
|
215
|
+
# Opal ships a tiny `uri.rb` that does not define RFC2396_PARSER or
|
|
216
|
+
# DEFAULT_PARSER. Multiple gems reference URI::DEFAULT_PARSER at
|
|
217
|
+
# method default-value time (eager on first call) or at constant
|
|
218
|
+
# lookup time:
|
|
219
|
+
# - rack/utils.rb (already handled in vendor patch)
|
|
220
|
+
# - mustermann/ast/translator.rb line 121: def escape(char, parser: URI::DEFAULT_PARSER, ...)
|
|
221
|
+
# - mustermann/pattern.rb line 12: @@uri ||= URI::Parser.new
|
|
222
|
+
#
|
|
223
|
+
# We install a module-shaped URI::DEFAULT_PARSER that wraps CGI so
|
|
224
|
+
# that gems that only call escape / unescape / regexp[:UNSAFE] on it
|
|
225
|
+
# continue to work.
|
|
226
|
+
require 'uri' rescue nil
|
|
227
|
+
require 'cgi'
|
|
228
|
+
|
|
229
|
+
module ::URI
|
|
230
|
+
unless const_defined?(:DEFAULT_PARSER)
|
|
231
|
+
DEFAULT_PARSER = Module.new do
|
|
232
|
+
UNSAFE = Regexp.compile('[^\-_.!~*\'()a-zA-Z0-9;/?:@&=+$,\[\]]').freeze
|
|
233
|
+
|
|
234
|
+
def self.regexp
|
|
235
|
+
{ UNSAFE: UNSAFE }
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def self.escape(s, unsafe = UNSAFE)
|
|
239
|
+
CGI.escape(s.to_s)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def self.unescape(s)
|
|
243
|
+
CGI.unescape(s.to_s)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# CRuby's URI.decode_www_form_component / encode_www_form_component are used
|
|
249
|
+
# by Rack::Utils#unescape / Rack::Utils#escape. Opal's `uri` stdlib omits
|
|
250
|
+
# them. Back them with CGI so that Rack's query-string parser works for
|
|
251
|
+
# any request with a body / query (Sinatra's `request.body.read` path
|
|
252
|
+
# eventually walks through this code).
|
|
253
|
+
#
|
|
254
|
+
# IMPORTANT (Opal): CGI.unescape maps to JS **decodeURI**, which does NOT
|
|
255
|
+
# decode `%2F` (`/`). RFC 3986 decodeURI reserves those escapes; Rack form
|
|
256
|
+
# bodies need **decodeURIComponent** semantics (same as CGI.unescapeURIComponent).
|
|
257
|
+
# Without this, HTML like `</h1>` survives as literal `<%2Fh1>` in params.
|
|
258
|
+
def self.decode_www_form_component(str, _enc = nil)
|
|
259
|
+
s = str.to_s.tr('+', ' ')
|
|
260
|
+
CGI.unescapeURIComponent(s)
|
|
261
|
+
rescue ::Exception
|
|
262
|
+
str.to_s
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
unless respond_to?(:encode_www_form_component)
|
|
266
|
+
def self.encode_www_form_component(str, _enc = nil)
|
|
267
|
+
CGI.escape(str.to_s)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
unless const_defined?(:RFC2396_PARSER)
|
|
272
|
+
RFC2396_PARSER = DEFAULT_PARSER
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
unless const_defined?(:Parser)
|
|
276
|
+
# Some gems instantiate URI::Parser.new directly. Return the
|
|
277
|
+
# singleton module which has the same surface area.
|
|
278
|
+
class Parser
|
|
279
|
+
def self.new(*)
|
|
280
|
+
::URI::DEFAULT_PARSER
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Rack::Protection::JsonCsrf#has_vector? does `rescue URI::InvalidURIError`,
|
|
286
|
+
# so the constant needs to exist even if the referrer is actually valid.
|
|
287
|
+
unless const_defined?(:InvalidURIError)
|
|
288
|
+
class InvalidURIError < StandardError; end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
unless const_defined?(:Error)
|
|
292
|
+
class Error < StandardError; end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Opal's stdlib does not implement URI.parse. Rack::Protection calls
|
|
296
|
+
# `URI.parse(env['HTTP_REFERER']).host`. A tiny JS-URL-backed parser is
|
|
297
|
+
# enough to cover that and any equivalent `host`-only usage.
|
|
298
|
+
class Generic
|
|
299
|
+
attr_reader :host, :scheme, :port, :path, :query, :fragment
|
|
300
|
+
|
|
301
|
+
def initialize(host:, scheme:, port:, path:, query:, fragment:)
|
|
302
|
+
@host = host
|
|
303
|
+
@scheme = scheme
|
|
304
|
+
@port = port
|
|
305
|
+
@path = path
|
|
306
|
+
@query = query
|
|
307
|
+
@fragment = fragment
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def self.parse(str)
|
|
312
|
+
s = str.to_s
|
|
313
|
+
return Generic.new(host: nil, scheme: nil, port: nil, path: '', query: nil, fragment: nil) if s.empty?
|
|
314
|
+
|
|
315
|
+
js_url = `
|
|
316
|
+
(function() {
|
|
317
|
+
try { return new URL(#{s}); }
|
|
318
|
+
catch (e) {
|
|
319
|
+
try { return new URL(#{s}, "http://__homura.invalid/"); }
|
|
320
|
+
catch (e2) { return null; }
|
|
321
|
+
}
|
|
322
|
+
})()
|
|
323
|
+
`
|
|
324
|
+
raise ::URI::InvalidURIError, "bad URI(is not URI?): #{s}" if `#{js_url} == null`
|
|
325
|
+
|
|
326
|
+
host = `#{js_url}.host` || ''
|
|
327
|
+
host = nil if host == '' || host.include?('__homura.invalid')
|
|
328
|
+
scheme = `#{js_url}.protocol` || ''
|
|
329
|
+
scheme = scheme.sub(/:$/, '')
|
|
330
|
+
scheme = nil if scheme == ''
|
|
331
|
+
port_raw = `#{js_url}.port` || ''
|
|
332
|
+
port = port_raw == '' ? nil : port_raw.to_i
|
|
333
|
+
path = `#{js_url}.pathname` || ''
|
|
334
|
+
query = `#{js_url}.search` || ''
|
|
335
|
+
query = query.sub(/^\?/, '')
|
|
336
|
+
query = nil if query == ''
|
|
337
|
+
frag = `#{js_url}.hash` || ''
|
|
338
|
+
frag = frag.sub(/^#/, '')
|
|
339
|
+
frag = nil if frag == ''
|
|
340
|
+
|
|
341
|
+
Generic.new(host: host, scheme: scheme, port: port, path: path, query: query, fragment: frag)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Net::HTTP.get(URI('https://...')) is the canonical entry point in
|
|
345
|
+
# CRuby Ruby code. CRuby resolves `URI('...')` via Kernel#URI, which
|
|
346
|
+
# is defined in `uri/common.rb` as `URI.parse(arg)`. Opal's stdlib
|
|
347
|
+
# omits that; install it here so vendored gems (and our Net::HTTP
|
|
348
|
+
# shim) can use the idiomatic short form.
|
|
349
|
+
def self.HTTP_class_for(scheme)
|
|
350
|
+
HTTP if scheme == 'http'
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Kernel#URI(string) — CRuby alias for URI.parse(string). Phase 6
|
|
355
|
+
# requires it for `Net::HTTP.get(URI('https://...'))` to work.
|
|
356
|
+
module ::Kernel
|
|
357
|
+
def URI(arg)
|
|
358
|
+
return arg if arg.is_a?(::URI::Generic)
|
|
359
|
+
::URI.parse(arg.to_s)
|
|
360
|
+
end
|
|
361
|
+
module_function :URI
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# -----------------------------------------------------------------
|
|
365
|
+
# IO.read / IO.binread / File.read / File.binread — Workers have no FS
|
|
366
|
+
# -----------------------------------------------------------------
|
|
367
|
+
# Opal does not implement IO.read / File.read. On Cloudflare Workers
|
|
368
|
+
# there is no writable filesystem anyway, so any code that tries to
|
|
369
|
+
# read a local file will fail. Gems that *optionally* read a file
|
|
370
|
+
# (Sinatra's inline_templates= for example) expect an Errno::ENOENT
|
|
371
|
+
# and rescue it silently; plain method_missing breaks that rescue.
|
|
372
|
+
#
|
|
373
|
+
# Install Errno::ENOENT-raising stubs so callers that rescue the
|
|
374
|
+
# standard not-found exception take the silent path. Callers that
|
|
375
|
+
# do not rescue get a clear, specific error instead of method_missing.
|
|
376
|
+
module ::Kernel
|
|
377
|
+
module_function
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# -----------------------------------------------------------------
|
|
381
|
+
# SecureRandom — Web Crypto API with graceful fallback
|
|
382
|
+
# -----------------------------------------------------------------
|
|
383
|
+
# Cloudflare Workers forbids async I/O AND random-value generation at
|
|
384
|
+
# module-load time (global scope). Sinatra eagerly generates a session
|
|
385
|
+
# secret at class-body time via SecureRandom.hex(64), which crashes
|
|
386
|
+
# with "Disallowed operation called within global scope" on Workers.
|
|
387
|
+
#
|
|
388
|
+
# We provide a SecureRandom implementation that:
|
|
389
|
+
# 1. Tries `crypto.getRandomValues` via Web Crypto (works inside fetch
|
|
390
|
+
# handlers on Workers and everywhere on Node/browsers).
|
|
391
|
+
# 2. Catches any failure (including the Workers global-scope
|
|
392
|
+
# restriction) and falls back to a deterministic all-zero string.
|
|
393
|
+
#
|
|
394
|
+
# Sinatra's session_secret therefore becomes "000…0" for the duration
|
|
395
|
+
# of the isolate lifetime when no request is in flight. That is the
|
|
396
|
+
# same strength CRuby Sinatra gives you when SecureRandom is unavailable
|
|
397
|
+
# (it falls back to `Kernel.rand`), so we are not reducing security
|
|
398
|
+
# beyond upstream's own fallback path.
|
|
399
|
+
# Ensure our Digest/Zlib/Tempfile/Tilt stubs from vendor/ are available
|
|
400
|
+
# everywhere, even when a gem references `Digest::SHA1` at class body
|
|
401
|
+
# time without explicitly `require 'digest'`-ing first.
|
|
402
|
+
require 'digest'
|
|
403
|
+
require 'digest/sha2'
|
|
404
|
+
require 'zlib'
|
|
405
|
+
require 'tempfile'
|
|
406
|
+
require 'tilt'
|
|
407
|
+
|
|
408
|
+
module ::SecureRandom
|
|
409
|
+
# Raised when neither node:crypto.randomBytes nor Web Crypto
|
|
410
|
+
# getRandomValues is available. We FAIL CLOSED rather than return
|
|
411
|
+
# predictable bytes — silent degradation would let downstream code
|
|
412
|
+
# generate predictable session secrets, JWT signing keys, IVs, etc.
|
|
413
|
+
#
|
|
414
|
+
# NOTE: extends NotImplementedError on purpose. CRuby's SecureRandom
|
|
415
|
+
# raises NotImplementedError when no random device is available,
|
|
416
|
+
# and several gems (most notably Sinatra at line 1988 of base.rb)
|
|
417
|
+
# rescue NotImplementedError to fall back to Kernel.rand for
|
|
418
|
+
# module-load-time secrets. Cloudflare Workers blocks ALL random
|
|
419
|
+
# value generation at module-load (global) scope, so eager
|
|
420
|
+
# session_secret generation must take that fallback path.
|
|
421
|
+
# Request-time calls (where entropy is available) still get real
|
|
422
|
+
# cryptographic randomness; only the module-load case ever falls
|
|
423
|
+
# back, and Sinatra's session secret is the lone caller that
|
|
424
|
+
# actually does that gracefully.
|
|
425
|
+
class EntropyError < ::NotImplementedError; end
|
|
426
|
+
|
|
427
|
+
def self.random_bytes(n = 16)
|
|
428
|
+
n = n.to_i
|
|
429
|
+
n = 16 if n <= 0
|
|
430
|
+
hex_string = secure_hex_bytes(n)
|
|
431
|
+
raise EntropyError, 'no source of cryptographic entropy available (node:crypto AND Web Crypto both unreachable)' if hex_string.nil?
|
|
432
|
+
[hex_string].pack('H*')
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def self.hex(n = 16)
|
|
436
|
+
n = n.to_i
|
|
437
|
+
n = 16 if n <= 0
|
|
438
|
+
out = secure_hex_bytes(n)
|
|
439
|
+
raise EntropyError, 'no source of cryptographic entropy available (node:crypto AND Web Crypto both unreachable)' if out.nil?
|
|
440
|
+
out
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def self.uuid
|
|
444
|
+
h = hex(16)
|
|
445
|
+
"#{h[0, 8]}-#{h[8, 4]}-4#{h[13, 3]}-#{h[16, 4]}-#{h[20, 12]}"
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def self.base64(n = 16)
|
|
449
|
+
require 'base64'
|
|
450
|
+
Base64.strict_encode64(random_bytes(n))
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def self.urlsafe_base64(n = 16, padding = false)
|
|
454
|
+
s = base64(n).tr('+/', '-_')
|
|
455
|
+
padding ? s : s.delete('=')
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def self.random_number(n = 0)
|
|
459
|
+
# Not used at class-init time; real implementations welcome.
|
|
460
|
+
0
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Returns a hex string of `n` random bytes, or nil when no entropy
|
|
464
|
+
# source is available. Tries node:crypto.randomBytes first (works
|
|
465
|
+
# on both Cloudflare Workers with `nodejs_compat` and Node.js),
|
|
466
|
+
# falls back to Web Crypto getRandomValues (works at request time
|
|
467
|
+
# on Workers and everywhere on browsers).
|
|
468
|
+
def self.secure_hex_bytes(n)
|
|
469
|
+
# Opal does not always auto-return backtick IIFEs; assign first
|
|
470
|
+
# so the method's last expression is a normal Ruby reference.
|
|
471
|
+
result = `(function(n) {
|
|
472
|
+
try {
|
|
473
|
+
if (typeof globalThis.__nodeCrypto__ !== 'undefined' && globalThis.__nodeCrypto__) {
|
|
474
|
+
return globalThis.__nodeCrypto__.randomBytes(n).toString('hex');
|
|
475
|
+
}
|
|
476
|
+
} catch (e) { /* fall through to Web Crypto */ }
|
|
477
|
+
try {
|
|
478
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
479
|
+
var bytes = new Uint8Array(n);
|
|
480
|
+
crypto.getRandomValues(bytes);
|
|
481
|
+
var out = '';
|
|
482
|
+
for (var i = 0; i < bytes.length; i++) {
|
|
483
|
+
var h = bytes[i].toString(16);
|
|
484
|
+
if (h.length < 2) h = '0' + h;
|
|
485
|
+
out += h;
|
|
486
|
+
}
|
|
487
|
+
return out;
|
|
488
|
+
}
|
|
489
|
+
} catch (e) {
|
|
490
|
+
// Workers blocks getRandomValues at module-load scope; fall through.
|
|
491
|
+
}
|
|
492
|
+
return nil; // Opal nil singleton, not JS null — so .nil? works
|
|
493
|
+
})(#{n})`
|
|
494
|
+
result
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# -----------------------------------------------------------------
|
|
499
|
+
# Phase 7: Array#pack('H*') / String#unpack1('H*') for hex<->bin.
|
|
500
|
+
# Opal's pack.rb / unpack.rb don't register the 'H' directive
|
|
501
|
+
# ("hex string, high nibble first"). Crypto code (Digest, OpenSSL,
|
|
502
|
+
# jwt) heavily uses these to convert between binary and hex, so we
|
|
503
|
+
# add a minimal handler that intercepts the "H*" / "H<n>" format
|
|
504
|
+
# and falls back to the original implementation for everything else.
|
|
505
|
+
# -----------------------------------------------------------------
|
|
506
|
+
|
|
507
|
+
class ::Array
|
|
508
|
+
alias_method :pack_without_homura_hex, :pack
|
|
509
|
+
|
|
510
|
+
# `H*` consumes the entire hex string. `H<n>` consumes exactly `n`
|
|
511
|
+
# nibbles (= n/2 bytes, rounded down). Matches CRuby semantics so
|
|
512
|
+
# `[hex].pack('H4')` yields the first 2 bytes — Copilot caught
|
|
513
|
+
# this divergence in the initial Phase 7 PR.
|
|
514
|
+
def pack(format)
|
|
515
|
+
fmt = format.to_s
|
|
516
|
+
return pack_without_homura_hex(format) unless fmt == 'H*' || fmt =~ /\AH(\d+)\z/
|
|
517
|
+
|
|
518
|
+
hex = self.first.to_s
|
|
519
|
+
nibble_count = if fmt == 'H*'
|
|
520
|
+
hex.length
|
|
521
|
+
else
|
|
522
|
+
[fmt[1..-1].to_i, hex.length].min
|
|
523
|
+
end
|
|
524
|
+
nibble_count -= 1 if nibble_count.odd? # round down to whole bytes
|
|
525
|
+
out = ''
|
|
526
|
+
i = 0
|
|
527
|
+
while i < nibble_count
|
|
528
|
+
out = out + hex[i, 2].to_i(16).chr
|
|
529
|
+
i += 2
|
|
530
|
+
end
|
|
531
|
+
out
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
class ::String
|
|
536
|
+
alias_method :unpack1_without_homura_hex, :unpack1
|
|
537
|
+
|
|
538
|
+
# `unpack1('H*')` returns one hex pair per byte. CRuby treats the
|
|
539
|
+
# receiver as a raw byte sequence (encoding ASCII-8BIT). Opal stores
|
|
540
|
+
# all Strings as JS Strings (UTF-16 chars) and reports `bytesize` as
|
|
541
|
+
# the UTF-8 encoded byte count, which double-counts chars > 0x7F.
|
|
542
|
+
#
|
|
543
|
+
# For our crypto code, every "binary" String comes from
|
|
544
|
+
# `[hex].pack('H*')` or other functions that pack each byte (0..255)
|
|
545
|
+
# as exactly one JS char. We therefore iterate by char (`length`)
|
|
546
|
+
# and read each char's UTF-16 code unit as the byte value. This
|
|
547
|
+
# matches CRuby's behavior for ASCII-8BIT encoded strings.
|
|
548
|
+
#
|
|
549
|
+
# `H*` produces 2 hex chars per byte. `H<n>` truncates to the first
|
|
550
|
+
# `n` nibbles (rounded down to whole bytes for an odd `n`).
|
|
551
|
+
def unpack1(format)
|
|
552
|
+
fmt = format.to_s
|
|
553
|
+
return unpack1_without_homura_hex(format) unless fmt == 'H*' || fmt =~ /\AH(\d+)\z/
|
|
554
|
+
|
|
555
|
+
requested_nibbles = if fmt == 'H*'
|
|
556
|
+
self.length * 2
|
|
557
|
+
else
|
|
558
|
+
fmt[1..-1].to_i
|
|
559
|
+
end
|
|
560
|
+
out = ''
|
|
561
|
+
i = 0
|
|
562
|
+
n = self.length
|
|
563
|
+
while i < n && out.length < requested_nibbles
|
|
564
|
+
b = `(#{self}.charCodeAt(#{i}) & 0xff)`
|
|
565
|
+
h = b.to_s(16)
|
|
566
|
+
h = '0' + h if h.length == 1
|
|
567
|
+
out = out + h
|
|
568
|
+
i += 1
|
|
569
|
+
end
|
|
570
|
+
out[0, requested_nibbles]
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
class ::IO
|
|
575
|
+
def self.read(*args)
|
|
576
|
+
raise ::Errno::ENOENT, args.first.to_s
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def self.binread(*args)
|
|
580
|
+
raise ::Errno::ENOENT, args.first.to_s
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# File inherits from IO in CRuby; in Opal File is its own class.
|
|
585
|
+
# Install the same stubs on File defensively.
|
|
586
|
+
begin
|
|
587
|
+
file_class = ::File
|
|
588
|
+
unless file_class.respond_to?(:read) && !file_class.method(:read).source_location.nil?
|
|
589
|
+
def file_class.read(*args)
|
|
590
|
+
raise ::Errno::ENOENT, args.first.to_s
|
|
591
|
+
end
|
|
592
|
+
def file_class.binread(*args)
|
|
593
|
+
raise ::Errno::ENOENT, args.first.to_s
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
unless file_class.respond_to?(:fnmatch)
|
|
597
|
+
def file_class.fnmatch(pattern, path, *)
|
|
598
|
+
# Very small fnmatch: supports `*` and `?` only, good enough for
|
|
599
|
+
# Sinatra's template extension matching.
|
|
600
|
+
regex = '\A'
|
|
601
|
+
i = 0
|
|
602
|
+
p = pattern.to_s
|
|
603
|
+
while i < p.length
|
|
604
|
+
c = p[i]
|
|
605
|
+
case c
|
|
606
|
+
when '*' then regex += '.*'
|
|
607
|
+
when '?' then regex += '.'
|
|
608
|
+
when '.', '(', ')', '[', ']', '+', '^', '$' then regex += "\\#{c}"
|
|
609
|
+
else regex += c
|
|
610
|
+
end
|
|
611
|
+
i += 1
|
|
612
|
+
end
|
|
613
|
+
regex += '\z'
|
|
614
|
+
!!(path.to_s =~ Regexp.new(regex))
|
|
615
|
+
end
|
|
616
|
+
def file_class.fnmatch?(pattern, path, *)
|
|
617
|
+
fnmatch(pattern, path)
|
|
618
|
+
end
|
|
619
|
+
end
|
|
620
|
+
rescue NameError
|
|
621
|
+
# File not available at this load point — ignore.
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
# Phase 13 (upstream Sinatra 4.2.1): Sinatra::IndifferentHash references
|
|
625
|
+
# Gem::Version at class body eval to gate the `except` override. Opal
|
|
626
|
+
# does not bundle RubyGems — pre-require our minimal stub so the
|
|
627
|
+
# reference resolves before upstream Sinatra loads.
|
|
628
|
+
require 'rubygems/version'
|
|
629
|
+
|
|
630
|
+
# Phase 13 originally required `opal-parser` because upstream Sinatra's
|
|
631
|
+
# `set` helper used `class_eval("def ...")` for primitive option values.
|
|
632
|
+
# Phase 15-Pre removes that string-eval path in `vendor/sinatra_upstream/base.rb`
|
|
633
|
+
# (Proc-based getters / predicate) so the Workers bundle no longer needs the
|
|
634
|
+
# full Opal compiler + whitequark parser at runtime.
|
|
635
|
+
|
|
636
|
+
[
|
|
637
|
+
:ISO_2022_JP,
|
|
638
|
+
:SHIFT_JIS, :Shift_JIS, :WINDOWS_31J, :CP932, :SJIS,
|
|
639
|
+
:EUC_JP, :EUC_KR, :EUC_CN, :EUC_TW,
|
|
640
|
+
:BIG5, :GB18030, :GBK, :GB2312,
|
|
641
|
+
:WINDOWS_1250, :WINDOWS_1251, :WINDOWS_1252, :WINDOWS_1253,
|
|
642
|
+
:WINDOWS_1254, :WINDOWS_1255, :WINDOWS_1256, :WINDOWS_1257, :WINDOWS_1258,
|
|
643
|
+
:KOI8_R, :KOI8_U,
|
|
644
|
+
:ISO_8859_2, :ISO_8859_3, :ISO_8859_4, :ISO_8859_5,
|
|
645
|
+
:ISO_8859_6, :ISO_8859_7, :ISO_8859_8, :ISO_8859_9,
|
|
646
|
+
:ISO_8859_10, :ISO_8859_11, :ISO_8859_13, :ISO_8859_14,
|
|
647
|
+
:ISO_8859_15, :ISO_8859_16,
|
|
648
|
+
:MACROMAN
|
|
649
|
+
].each do |name|
|
|
650
|
+
unless Encoding.const_defined?(name)
|
|
651
|
+
Encoding.const_set(name, Encoding::ASCII_8BIT)
|
|
652
|
+
end
|
|
653
|
+
end
|