homura-runtime 0.3.6 → 0.3.8
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 +4 -4
- data/CHANGELOG.md +20 -0
- data/exe/auto-await +42 -27
- data/exe/compile-assets +46 -37
- data/exe/compile-erb +86 -61
- data/exe/homura-build +223 -119
- data/lib/homura/runtime/ai.rb +316 -22
- data/lib/homura/runtime/async_registry.rb +135 -98
- data/lib/homura/runtime/auto_await/analyzer.rb +34 -19
- data/lib/homura/runtime/auto_await/transformer.rb +1 -1
- data/lib/homura/runtime/build_support.rb +74 -38
- data/lib/homura/runtime/cache.rb +29 -22
- data/lib/homura/runtime/durable_object.rb +110 -56
- data/lib/homura/runtime/email.rb +28 -14
- data/lib/homura/runtime/http.rb +5 -4
- data/lib/homura/runtime/multipart.rb +47 -47
- data/lib/homura/runtime/queue.rb +82 -29
- data/lib/homura/runtime/scheduled.rb +29 -19
- data/lib/homura/runtime/stream.rb +30 -24
- data/lib/homura/runtime/version.rb +1 -1
- data/lib/homura/runtime.rb +351 -131
- data/lib/homura_vendor_tempfile.rb +5 -4
- data/lib/homura_vendor_tilt.rb +4 -3
- data/lib/homura_vendor_zlib.rb +20 -13
- data/lib/opal_patches.rb +196 -109
- metadata +1 -1
|
@@ -41,15 +41,17 @@
|
|
|
41
41
|
# HTTP fetch-style interaction with DO instances, which is enough for
|
|
42
42
|
# counters / session state / rate limiters.
|
|
43
43
|
|
|
44
|
-
require
|
|
44
|
+
require "json"
|
|
45
45
|
|
|
46
46
|
module Cloudflare
|
|
47
47
|
class DurableObjectError < StandardError
|
|
48
48
|
attr_reader :operation, :do_class
|
|
49
49
|
def initialize(message, operation: nil, do_class: nil)
|
|
50
50
|
@operation = operation
|
|
51
|
-
@do_class
|
|
52
|
-
super(
|
|
51
|
+
@do_class = do_class
|
|
52
|
+
super(
|
|
53
|
+
"[Cloudflare::DurableObject] class=#{do_class || "?"} op=#{operation || "fetch"}: #{message}"
|
|
54
|
+
)
|
|
53
55
|
end
|
|
54
56
|
end
|
|
55
57
|
|
|
@@ -136,7 +138,7 @@ module Cloudflare
|
|
|
136
138
|
# responses — the 101 Response carries its WebSocket in a
|
|
137
139
|
# `.webSocket` property that disappears if we reconstruct the
|
|
138
140
|
# Response via the HTTPResponse wrapper.
|
|
139
|
-
def fetch_raw(url_or_request, method:
|
|
141
|
+
def fetch_raw(url_or_request, method: "GET", headers: nil, body: nil)
|
|
140
142
|
hdrs = headers || {}
|
|
141
143
|
method_str = method.to_s.upcase
|
|
142
144
|
js_headers = Cloudflare::HTTP.ruby_headers_to_js(hdrs)
|
|
@@ -152,7 +154,7 @@ module Cloudflare
|
|
|
152
154
|
# the prefix), so user code can use any pathname it wants as its
|
|
153
155
|
# internal DO command channel. Returns a JS Promise the caller
|
|
154
156
|
# `__await__`s to get a `Cloudflare::HTTPResponse`.
|
|
155
|
-
def fetch(url_or_request, method:
|
|
157
|
+
def fetch(url_or_request, method: "GET", headers: nil, body: nil)
|
|
156
158
|
js_stub = @js
|
|
157
159
|
hdrs = headers || {}
|
|
158
160
|
method_str = method.to_s.upcase
|
|
@@ -161,11 +163,12 @@ module Cloudflare
|
|
|
161
163
|
url_str = url_or_request.to_s
|
|
162
164
|
err_klass = Cloudflare::DurableObjectError
|
|
163
165
|
response_klass = Cloudflare::HTTPResponse
|
|
164
|
-
do_class_label =
|
|
166
|
+
do_class_label = "DurableObjectStub"
|
|
165
167
|
|
|
166
168
|
# Single-line IIFE — see `lib/homura/runtime/cache.rb#put`
|
|
167
169
|
# for why Opal can silently drop a multi-line x-string Promise.
|
|
168
|
-
js_promise =
|
|
170
|
+
js_promise =
|
|
171
|
+
`(async function(stub, url_str, method_str, js_headers, js_body, Kernel, err_klass, do_class_label) { var init = { method: method_str, headers: js_headers }; if (js_body !== null && js_body !== undefined && js_body !== Opal.nil) { init.body = js_body; } var resp; try { resp = await stub.fetch(url_str, init); } catch (e) { Kernel.$raise(err_klass.$new(e && e.message ? e.message : String(e), Opal.hash({ operation: 'stub.fetch', do_class: do_class_label }))); } var text = ''; try { text = await resp.text(); } catch (_) { text = ''; } var hk = []; var hv = []; if (resp.headers && typeof resp.headers.forEach === 'function') { resp.headers.forEach(function(v, k) { hk.push(String(k).toLowerCase()); hv.push(String(v)); }); } return { status: resp.status|0, text: text, hkeys: hk, hvals: hv }; })(#{js_stub}, #{url_str}, #{method_str}, #{js_headers}, #{js_body}, #{Kernel}, #{err_klass}, #{do_class_label})`
|
|
169
172
|
|
|
170
173
|
js_result = js_promise.__await__
|
|
171
174
|
hkeys = `#{js_result}.hkeys`
|
|
@@ -179,39 +182,39 @@ module Cloudflare
|
|
|
179
182
|
end
|
|
180
183
|
|
|
181
184
|
response_klass.new(
|
|
182
|
-
status:
|
|
185
|
+
status: `#{js_result}.status`,
|
|
183
186
|
headers: h,
|
|
184
|
-
body:
|
|
185
|
-
url:
|
|
187
|
+
body: `#{js_result}.text`,
|
|
188
|
+
url: url_str
|
|
186
189
|
)
|
|
187
190
|
end
|
|
188
191
|
|
|
189
|
-
def request(path, method:
|
|
192
|
+
def request(path, method: "GET", headers: nil, body: nil)
|
|
190
193
|
hdrs = headers ? headers.dup : {}
|
|
191
194
|
request_body = body
|
|
192
195
|
if body.is_a?(Hash) || body.is_a?(Array)
|
|
193
196
|
request_body = body.to_json
|
|
194
|
-
unless hdrs.key?(
|
|
195
|
-
hdrs[
|
|
197
|
+
unless hdrs.key?("content-type") || hdrs.key?("Content-Type")
|
|
198
|
+
hdrs["content-type"] = "application/json"
|
|
196
199
|
end
|
|
197
200
|
end
|
|
198
201
|
fetch(path, method: method, headers: hdrs, body: request_body)
|
|
199
202
|
end
|
|
200
203
|
|
|
201
204
|
def get(path, headers: nil)
|
|
202
|
-
request(path, method:
|
|
205
|
+
request(path, method: "GET", headers: headers)
|
|
203
206
|
end
|
|
204
207
|
|
|
205
208
|
def post(path, body = nil, headers: nil)
|
|
206
|
-
request(path, method:
|
|
209
|
+
request(path, method: "POST", headers: headers, body: body)
|
|
207
210
|
end
|
|
208
211
|
|
|
209
212
|
def put(path, body = nil, headers: nil)
|
|
210
|
-
request(path, method:
|
|
213
|
+
request(path, method: "PUT", headers: headers, body: body)
|
|
211
214
|
end
|
|
212
215
|
|
|
213
216
|
def delete(path, headers: nil)
|
|
214
|
-
request(path, method:
|
|
217
|
+
request(path, method: "DELETE", headers: headers)
|
|
215
218
|
end
|
|
216
219
|
end
|
|
217
220
|
|
|
@@ -251,12 +254,15 @@ module Cloudflare
|
|
|
251
254
|
# The block must return a Rack-style triple `[status, headers, body]`.
|
|
252
255
|
# `body` may be a String or an object that responds to `to_s`.
|
|
253
256
|
def self.define(class_name, &block)
|
|
254
|
-
raise ArgumentError,
|
|
255
|
-
|
|
257
|
+
raise ArgumentError, "define requires a block" unless block
|
|
258
|
+
unless class_name.is_a?(String)
|
|
259
|
+
raise ArgumentError, "class_name must be a String"
|
|
260
|
+
end
|
|
256
261
|
@handlers ||= {}
|
|
257
262
|
# Wrap via define_method so Opal's `# await: true` picks it up as
|
|
258
263
|
# async (same trick Sinatra::Scheduled uses for its jobs).
|
|
259
|
-
method_name =
|
|
264
|
+
method_name =
|
|
265
|
+
"__do_handler_#{class_name.gsub(/[^A-Za-z0-9_]/, "_")}".to_sym
|
|
260
266
|
DurableObjectRequestContext.send(:define_method, method_name, &block)
|
|
261
267
|
unbound = DurableObjectRequestContext.instance_method(method_name)
|
|
262
268
|
DurableObjectRequestContext.send(:remove_method, method_name)
|
|
@@ -279,12 +285,17 @@ module Cloudflare
|
|
|
279
285
|
# class (wired by the exported HomuraCounterDO in
|
|
280
286
|
# src/worker.mjs). Return value is ignored — the runtime doesn't
|
|
281
287
|
# expect a body.
|
|
282
|
-
def self.define_web_socket_handlers(
|
|
288
|
+
def self.define_web_socket_handlers(
|
|
289
|
+
class_name,
|
|
290
|
+
on_message: nil,
|
|
291
|
+
on_close: nil,
|
|
292
|
+
on_error: nil
|
|
293
|
+
)
|
|
283
294
|
@ws_handlers ||= {}
|
|
284
295
|
@ws_handlers[class_name] = {
|
|
285
296
|
on_message: on_message,
|
|
286
|
-
on_close:
|
|
287
|
-
on_error:
|
|
297
|
+
on_close: on_close,
|
|
298
|
+
on_error: on_error
|
|
288
299
|
}.compact
|
|
289
300
|
nil
|
|
290
301
|
end
|
|
@@ -303,8 +314,12 @@ module Cloudflare
|
|
|
303
314
|
def self.dispatch_js(class_name, js_state, js_env, js_request, body_text)
|
|
304
315
|
handler = handler_for(class_name)
|
|
305
316
|
if handler.nil?
|
|
306
|
-
body = {
|
|
307
|
-
|
|
317
|
+
body = {
|
|
318
|
+
"error" => "no Ruby handler for DurableObject class #{class_name}"
|
|
319
|
+
}.to_json
|
|
320
|
+
return(
|
|
321
|
+
build_js_response(500, { "content-type" => "application/json" }, body)
|
|
322
|
+
)
|
|
308
323
|
end
|
|
309
324
|
|
|
310
325
|
state = DurableObjectState.new(js_state)
|
|
@@ -334,12 +349,12 @@ module Cloudflare
|
|
|
334
349
|
[result[0].to_i, result[1] || {}, result[2].to_s]
|
|
335
350
|
elsif result.is_a?(Hash)
|
|
336
351
|
[
|
|
337
|
-
(result[
|
|
338
|
-
result[
|
|
339
|
-
(result[
|
|
352
|
+
(result["status"] || result[:status] || 200).to_i,
|
|
353
|
+
result["headers"] || result[:headers] || {},
|
|
354
|
+
(result["body"] || result[:body] || "").to_s
|
|
340
355
|
]
|
|
341
356
|
else
|
|
342
|
-
[200, {
|
|
357
|
+
[200, { "content-type" => "text/plain; charset=utf-8" }, result.to_s]
|
|
343
358
|
end
|
|
344
359
|
end
|
|
345
360
|
|
|
@@ -358,7 +373,13 @@ module Cloudflare
|
|
|
358
373
|
# WebSocket dispatchers — called from the JS DO class's
|
|
359
374
|
# `webSocketMessage` / `webSocketClose` / `webSocketError`
|
|
360
375
|
# methods. Each returns a JS Promise that resolves to undefined.
|
|
361
|
-
def self.dispatch_ws_message(
|
|
376
|
+
def self.dispatch_ws_message(
|
|
377
|
+
class_name,
|
|
378
|
+
js_ws,
|
|
379
|
+
js_message,
|
|
380
|
+
js_state,
|
|
381
|
+
js_env
|
|
382
|
+
)
|
|
362
383
|
h = web_socket_handlers_for(class_name)
|
|
363
384
|
return nil if h.nil? || h[:on_message].nil?
|
|
364
385
|
state = DurableObjectState.new(js_state)
|
|
@@ -366,7 +387,15 @@ module Cloudflare
|
|
|
366
387
|
nil
|
|
367
388
|
end
|
|
368
389
|
|
|
369
|
-
def self.dispatch_ws_close(
|
|
390
|
+
def self.dispatch_ws_close(
|
|
391
|
+
class_name,
|
|
392
|
+
js_ws,
|
|
393
|
+
code,
|
|
394
|
+
reason,
|
|
395
|
+
was_clean,
|
|
396
|
+
js_state,
|
|
397
|
+
js_env
|
|
398
|
+
)
|
|
370
399
|
h = web_socket_handlers_for(class_name)
|
|
371
400
|
return nil if h.nil? || h[:on_close].nil?
|
|
372
401
|
state = DurableObjectState.new(js_state)
|
|
@@ -411,7 +440,7 @@ module Cloudflare
|
|
|
411
440
|
|
|
412
441
|
def initialize(js_state)
|
|
413
442
|
@js_state = js_state
|
|
414
|
-
@storage
|
|
443
|
+
@storage = DurableObjectStorage.new(`#{js_state} && #{js_state}.storage`)
|
|
415
444
|
end
|
|
416
445
|
|
|
417
446
|
# Unique id of this DO instance as a hex String.
|
|
@@ -442,7 +471,10 @@ module Cloudflare
|
|
|
442
471
|
js_state = @js_state
|
|
443
472
|
if tags && !tags.empty?
|
|
444
473
|
js_tags = `([])`
|
|
445
|
-
tags.each
|
|
474
|
+
tags.each do |t|
|
|
475
|
+
ts = t.to_s
|
|
476
|
+
`#{js_tags}.push(#{ts})`
|
|
477
|
+
end
|
|
446
478
|
`#{js_state}.acceptWebSocket(#{js_ws}, #{js_tags})`
|
|
447
479
|
else
|
|
448
480
|
`#{js_state}.acceptWebSocket(#{js_ws})`
|
|
@@ -455,12 +487,13 @@ module Cloudflare
|
|
|
455
487
|
# `getWebSockets(tag)`.
|
|
456
488
|
def web_sockets(tag: nil)
|
|
457
489
|
js_state = @js_state
|
|
458
|
-
js_arr =
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
490
|
+
js_arr =
|
|
491
|
+
if tag
|
|
492
|
+
ts = tag.to_s
|
|
493
|
+
`(#{js_state}.getWebSockets ? #{js_state}.getWebSockets(#{ts}) : [])`
|
|
494
|
+
else
|
|
495
|
+
`(#{js_state}.getWebSockets ? #{js_state}.getWebSockets() : [])`
|
|
496
|
+
end
|
|
464
497
|
out = []
|
|
465
498
|
len = `#{js_arr}.length`
|
|
466
499
|
i = 0
|
|
@@ -494,7 +527,7 @@ module Cloudflare
|
|
|
494
527
|
def put(key, value)
|
|
495
528
|
js = @js
|
|
496
529
|
err_klass = Cloudflare::DurableObjectError
|
|
497
|
-
js_value = value.nil? ?
|
|
530
|
+
js_value = value.nil? ? "null" : value.to_json
|
|
498
531
|
`#{js}.put(#{key.to_s}, #{js_value}).catch(function(e) { #{Kernel}.$raise(#{err_klass}.$new(e && e.message ? e.message : String(e), Opal.hash({ operation: 'storage.put' }))); })`
|
|
499
532
|
end
|
|
500
533
|
|
|
@@ -530,11 +563,12 @@ module Cloudflare
|
|
|
530
563
|
err_klass = Cloudflare::DurableObjectError
|
|
531
564
|
js_opts = `({})`
|
|
532
565
|
`#{js_opts}.prefix = #{prefix.to_s}` unless prefix.nil?
|
|
533
|
-
`#{js_opts}.limit = #{limit.to_i}`
|
|
534
|
-
`#{js_opts}.reverse = #{!!reverse}`
|
|
535
|
-
`#{js_opts}.start = #{start.to_s}`
|
|
566
|
+
`#{js_opts}.limit = #{limit.to_i}` unless limit.nil?
|
|
567
|
+
`#{js_opts}.reverse = #{!!reverse}` unless reverse.nil?
|
|
568
|
+
`#{js_opts}.start = #{start.to_s}` unless start.nil?
|
|
536
569
|
`#{js_opts}.end = #{end_key.to_s}` unless end_key.nil?
|
|
537
|
-
js_promise =
|
|
570
|
+
js_promise =
|
|
571
|
+
`#{js}.list(#{js_opts}).catch(function(e) { #{Kernel}.$raise(#{err_klass}.$new(e && e.message ? e.message : String(e), Opal.hash({ operation: 'storage.list' }))); })`
|
|
538
572
|
js_result = js_promise.__await__
|
|
539
573
|
out = {}
|
|
540
574
|
return out if `#{js_result} == null`
|
|
@@ -549,7 +583,7 @@ module Cloudflare
|
|
|
549
583
|
class DurableObjectRequest
|
|
550
584
|
attr_reader :js_request, :body
|
|
551
585
|
|
|
552
|
-
def initialize(js_request, body_text =
|
|
586
|
+
def initialize(js_request, body_text = "")
|
|
553
587
|
@js_request = js_request
|
|
554
588
|
@body = body_text.to_s
|
|
555
589
|
end
|
|
@@ -566,7 +600,7 @@ module Cloudflare
|
|
|
566
600
|
|
|
567
601
|
def path
|
|
568
602
|
u = url
|
|
569
|
-
return
|
|
603
|
+
return "" if u.nil? || u.empty?
|
|
570
604
|
begin
|
|
571
605
|
# Extract pathname via URL() so relative paths aren't mangled.
|
|
572
606
|
u_str = u
|
|
@@ -609,17 +643,37 @@ module Cloudflare
|
|
|
609
643
|
@request = request
|
|
610
644
|
end
|
|
611
645
|
|
|
612
|
-
def storage
|
|
613
|
-
|
|
614
|
-
|
|
646
|
+
def storage
|
|
647
|
+
@state.storage
|
|
648
|
+
end
|
|
649
|
+
def cf_env
|
|
650
|
+
env["cloudflare.env"]
|
|
651
|
+
end
|
|
652
|
+
def cf_ctx
|
|
653
|
+
env["cloudflare.ctx"]
|
|
654
|
+
end
|
|
615
655
|
|
|
616
|
-
def d1
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
def
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
def
|
|
656
|
+
def d1
|
|
657
|
+
env["cloudflare.DB"]
|
|
658
|
+
end
|
|
659
|
+
def db
|
|
660
|
+
d1
|
|
661
|
+
end
|
|
662
|
+
def kv
|
|
663
|
+
env["cloudflare.KV"]
|
|
664
|
+
end
|
|
665
|
+
def bucket
|
|
666
|
+
env["cloudflare.BUCKET"]
|
|
667
|
+
end
|
|
668
|
+
def ai
|
|
669
|
+
Cloudflare::Bindings.ai(env)
|
|
670
|
+
end
|
|
671
|
+
def send_email
|
|
672
|
+
env["cloudflare.SEND_EMAIL"]
|
|
673
|
+
end
|
|
674
|
+
def jobs_queue
|
|
675
|
+
env["cloudflare.QUEUE_JOBS"]
|
|
676
|
+
end
|
|
623
677
|
def durable_object(name, id_or_name = nil)
|
|
624
678
|
Cloudflare::Bindings.durable_object(env, name, id_or_name)
|
|
625
679
|
end
|
data/lib/homura/runtime/email.rb
CHANGED
|
@@ -42,19 +42,30 @@ module Cloudflare
|
|
|
42
42
|
def send(to:, from:, subject:, text: nil, html: nil, reply_to: nil)
|
|
43
43
|
js = @js
|
|
44
44
|
err_klass = Cloudflare::Email::Error
|
|
45
|
-
raise Error,
|
|
45
|
+
raise Error, "send_email binding not bound" unless available?
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
if subject.nil? || subject.to_s.strip.empty?
|
|
48
|
+
raise Error, "subject is required"
|
|
49
|
+
end
|
|
48
50
|
|
|
49
51
|
has_text = !(text.nil? || text.to_s.empty?)
|
|
50
52
|
has_html = !(html.nil? || html.to_s.empty?)
|
|
51
|
-
raise Error,
|
|
52
|
-
|
|
53
|
-
payload =
|
|
53
|
+
raise Error, "text or html is required" unless has_text || has_html
|
|
54
|
+
|
|
55
|
+
payload =
|
|
56
|
+
build_send_payload(
|
|
57
|
+
to: to,
|
|
58
|
+
from: from,
|
|
59
|
+
subject: subject.to_s,
|
|
60
|
+
text: text,
|
|
61
|
+
html: html,
|
|
62
|
+
reply_to: reply_to
|
|
63
|
+
)
|
|
54
64
|
|
|
55
65
|
cf = Cloudflare
|
|
56
66
|
# 多行 x-string をメソッド末尾に置くと Opal が Promise を返さない出力になることがあるため return を明示する。
|
|
57
|
-
return
|
|
67
|
+
return(
|
|
68
|
+
`(async function(binding, payload, Kernel, Err, cf) {
|
|
58
69
|
try {
|
|
59
70
|
var r = await binding.send(payload);
|
|
60
71
|
if (r == null || r === undefined) {
|
|
@@ -73,6 +84,7 @@ module Cloudflare
|
|
|
73
84
|
Kernel.$raise(Err.$new(msg, Opal.hash({ code: code })));
|
|
74
85
|
}
|
|
75
86
|
})(#{js}, #{payload}, #{Kernel}, #{err_klass}, #{cf})`
|
|
87
|
+
)
|
|
76
88
|
end
|
|
77
89
|
|
|
78
90
|
private
|
|
@@ -103,7 +115,7 @@ module Cloudflare
|
|
|
103
115
|
# Returns a JS array: mix of address strings and `{ email, name? }` objects (Workers API shape).
|
|
104
116
|
def normalize_to_js(raw)
|
|
105
117
|
entries = flatten_recipients(raw)
|
|
106
|
-
raise Error,
|
|
118
|
+
raise Error, "to is empty" if entries.empty?
|
|
107
119
|
|
|
108
120
|
arr = `([])`
|
|
109
121
|
entries.each do |e|
|
|
@@ -113,7 +125,9 @@ module Cloudflare
|
|
|
113
125
|
when Hash
|
|
114
126
|
js = `({})`
|
|
115
127
|
`#{js}.email = #{e[:email]}`
|
|
116
|
-
|
|
128
|
+
if e[:name] && !e[:name].to_s.strip.empty?
|
|
129
|
+
`#{js}.name = #{e[:name].to_s}`
|
|
130
|
+
end
|
|
117
131
|
`#{arr}.push(#{js})`
|
|
118
132
|
end
|
|
119
133
|
end
|
|
@@ -129,9 +143,9 @@ module Cloudflare
|
|
|
129
143
|
s = raw.strip
|
|
130
144
|
s.empty? ? [] : [s]
|
|
131
145
|
when Hash
|
|
132
|
-
em = raw[:email] || raw[
|
|
146
|
+
em = raw[:email] || raw["email"]
|
|
133
147
|
return [] if em.nil? || em.to_s.strip.empty?
|
|
134
|
-
nm = raw[:name] || raw[
|
|
148
|
+
nm = raw[:name] || raw["name"]
|
|
135
149
|
if nm.nil? || nm.to_s.strip.empty?
|
|
136
150
|
[em.to_s.strip]
|
|
137
151
|
else
|
|
@@ -162,12 +176,12 @@ module Cloudflare
|
|
|
162
176
|
case raw
|
|
163
177
|
when String
|
|
164
178
|
s = raw.strip
|
|
165
|
-
raise Error,
|
|
179
|
+
raise Error, "from address is empty" if s.empty?
|
|
166
180
|
return s
|
|
167
181
|
when Hash
|
|
168
|
-
em = raw[:email] || raw[
|
|
169
|
-
nm = raw[:name] || raw[
|
|
170
|
-
raise Error,
|
|
182
|
+
em = raw[:email] || raw["email"]
|
|
183
|
+
nm = raw[:name] || raw["name"]
|
|
184
|
+
raise Error, "from.email is required" if em.nil? || em.to_s.strip.empty?
|
|
171
185
|
js = `({})`
|
|
172
186
|
`#{js}.email = #{em.to_s.strip}`
|
|
173
187
|
`#{js}.name = #{nm.to_s}` unless nm.nil? || nm.to_s.strip.empty?
|
data/lib/homura/runtime/http.rb
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
# goes through `globalThis.fetch`, because Cloudflare Workers does not
|
|
16
16
|
# expose a TCP socket API.
|
|
17
17
|
|
|
18
|
-
require
|
|
18
|
+
require "json"
|
|
19
19
|
|
|
20
20
|
module Cloudflare
|
|
21
21
|
class HTTPError < StandardError
|
|
@@ -23,7 +23,7 @@ module Cloudflare
|
|
|
23
23
|
def initialize(message, url: nil, method: nil)
|
|
24
24
|
@url = url
|
|
25
25
|
@method = method
|
|
26
|
-
super("[Cloudflare::HTTP] #{method ||
|
|
26
|
+
super("[Cloudflare::HTTP] #{method || "GET"} #{url}: #{message}")
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
|
|
@@ -67,7 +67,7 @@ module Cloudflare
|
|
|
67
67
|
#
|
|
68
68
|
# The whole response body is awaited and returned as a String.
|
|
69
69
|
# Use `Cloudflare::HTTPResponse#body` to access raw text.
|
|
70
|
-
def self.fetch(url, method:
|
|
70
|
+
def self.fetch(url, method: "GET", headers: nil, body: nil)
|
|
71
71
|
hdrs = headers || DEFAULT_HEADERS
|
|
72
72
|
method_str = method.to_s.upcase
|
|
73
73
|
js_headers = ruby_headers_to_js(hdrs)
|
|
@@ -87,7 +87,8 @@ module Cloudflare
|
|
|
87
87
|
# expression. See the single-line IIFE pattern used in
|
|
88
88
|
# lib/homura/runtime/{cache,queue,durable_object}.rb#put for
|
|
89
89
|
# the alternative that survives either position. (Phase 11B audit.)
|
|
90
|
-
js_promise =
|
|
90
|
+
js_promise =
|
|
91
|
+
`
|
|
91
92
|
(async function() {
|
|
92
93
|
var init = { method: #{method_str}, headers: #{js_headers}, redirect: 'follow' };
|
|
93
94
|
if (#{js_body} !== nil && #{js_body} != null) { init.body = #{js_body}; }
|
|
@@ -43,12 +43,12 @@ module Cloudflare
|
|
|
43
43
|
class UploadedFile
|
|
44
44
|
attr_reader :filename, :content_type, :name, :head, :bytes_binstr
|
|
45
45
|
|
|
46
|
-
def initialize(filename:, content_type:, name:, head:
|
|
46
|
+
def initialize(filename:, content_type:, name:, head: "", bytes_binstr: "")
|
|
47
47
|
@filename = filename
|
|
48
|
-
@content_type = content_type ||
|
|
48
|
+
@content_type = content_type || "application/octet-stream"
|
|
49
49
|
@name = name
|
|
50
50
|
@head = head
|
|
51
|
-
@bytes_binstr = bytes_binstr ||
|
|
51
|
+
@bytes_binstr = bytes_binstr || ""
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
# Byte length of the part (not the JS string length — they're the
|
|
@@ -100,9 +100,9 @@ module Cloudflare
|
|
|
100
100
|
def to_h
|
|
101
101
|
{
|
|
102
102
|
filename: @filename,
|
|
103
|
-
type:
|
|
104
|
-
name:
|
|
105
|
-
head:
|
|
103
|
+
type: @content_type,
|
|
104
|
+
name: @name,
|
|
105
|
+
head: @head,
|
|
106
106
|
tempfile: self
|
|
107
107
|
}
|
|
108
108
|
end
|
|
@@ -127,7 +127,7 @@ module Cloudflare
|
|
|
127
127
|
def self.parse_boundary(content_type)
|
|
128
128
|
return nil if content_type.nil?
|
|
129
129
|
ct = content_type.to_s
|
|
130
|
-
return nil unless ct.downcase.include?(
|
|
130
|
+
return nil unless ct.downcase.include?("multipart/")
|
|
131
131
|
# Prefer the quoted form. The quoted value may contain any byte
|
|
132
132
|
# except a literal `"` (RFC 2046 §5.1.1 bans `"` in the value).
|
|
133
133
|
if (m = ct.match(/boundary="([^"]+)"/i))
|
|
@@ -151,20 +151,18 @@ module Cloudflare
|
|
|
151
151
|
return {} if boundary.nil?
|
|
152
152
|
return {} if body_binstr.nil? || body_binstr.empty?
|
|
153
153
|
|
|
154
|
-
sep
|
|
155
|
-
term
|
|
156
|
-
sep_line
|
|
157
|
-
sep_last
|
|
158
|
-
body
|
|
154
|
+
sep = "--" + boundary
|
|
155
|
+
term = "--" + boundary + "--"
|
|
156
|
+
sep_line = sep + CRLF
|
|
157
|
+
sep_last = sep + CRLF # the very first boundary may skip the leading CRLF
|
|
158
|
+
body = body_binstr.to_s
|
|
159
159
|
|
|
160
160
|
# Skip any preamble before the first boundary.
|
|
161
161
|
start_idx = body.index(sep)
|
|
162
162
|
return {} if start_idx.nil?
|
|
163
163
|
cursor = start_idx + sep.length
|
|
164
164
|
# consume possible CRLF right after the first boundary
|
|
165
|
-
if body[cursor, 2] == CRLF
|
|
166
|
-
cursor += 2
|
|
167
|
-
end
|
|
165
|
+
cursor += 2 if body[cursor, 2] == CRLF
|
|
168
166
|
|
|
169
167
|
parts = {}
|
|
170
168
|
|
|
@@ -181,28 +179,32 @@ module Cloudflare
|
|
|
181
179
|
headers_end = part.index(CRLF + CRLF)
|
|
182
180
|
if headers_end
|
|
183
181
|
raw_headers = part[0...headers_end]
|
|
184
|
-
raw_body
|
|
182
|
+
raw_body = part[(headers_end + 4)..-1] || ""
|
|
185
183
|
else
|
|
186
184
|
raw_headers = part
|
|
187
|
-
raw_body
|
|
185
|
+
raw_body = ""
|
|
188
186
|
end
|
|
189
187
|
|
|
190
188
|
disposition = nil
|
|
191
|
-
ctype
|
|
192
|
-
raw_headers
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
189
|
+
ctype = nil
|
|
190
|
+
raw_headers
|
|
191
|
+
.split(CRLF)
|
|
192
|
+
.each do |line|
|
|
193
|
+
name, value = line.split(":", 2)
|
|
194
|
+
next if name.nil? || value.nil?
|
|
195
|
+
name = name.strip.downcase
|
|
196
|
+
value = value.strip
|
|
197
|
+
case name
|
|
198
|
+
when "content-disposition"
|
|
199
|
+
disposition = value
|
|
200
|
+
when "content-type"
|
|
201
|
+
ctype = value
|
|
202
|
+
end
|
|
200
203
|
end
|
|
201
|
-
end
|
|
202
204
|
|
|
203
205
|
if disposition
|
|
204
|
-
field_name = extract_disposition_param(disposition,
|
|
205
|
-
filename
|
|
206
|
+
field_name = extract_disposition_param(disposition, "name")
|
|
207
|
+
filename = extract_disposition_param(disposition, "filename")
|
|
206
208
|
if field_name
|
|
207
209
|
if filename && !filename.empty?
|
|
208
210
|
parts[field_name] = UploadedFile.new(
|
|
@@ -220,12 +222,8 @@ module Cloudflare
|
|
|
220
222
|
|
|
221
223
|
cursor = next_sep + CRLF.length + sep.length
|
|
222
224
|
# Check whether this is the terminator `--boundary--`
|
|
223
|
-
if body[cursor, 2] ==
|
|
224
|
-
|
|
225
|
-
end
|
|
226
|
-
if body[cursor, 2] == CRLF
|
|
227
|
-
cursor += 2
|
|
228
|
-
end
|
|
225
|
+
break if body[cursor, 2] == "--"
|
|
226
|
+
cursor += 2 if body[cursor, 2] == CRLF
|
|
229
227
|
end
|
|
230
228
|
|
|
231
229
|
parts
|
|
@@ -274,14 +272,14 @@ module Cloudflare
|
|
|
274
272
|
#
|
|
275
273
|
# Called lazily from our patched Rack::Request#POST.
|
|
276
274
|
def self.rack_params(env)
|
|
277
|
-
cached = env[
|
|
275
|
+
cached = env["cloudflare.multipart"]
|
|
278
276
|
return cached if cached
|
|
279
277
|
|
|
280
|
-
ct = env[
|
|
281
|
-
return
|
|
278
|
+
ct = env["CONTENT_TYPE"]
|
|
279
|
+
return({}) unless ct && ct.to_s.downcase.include?("multipart/")
|
|
282
280
|
|
|
283
|
-
io = env[
|
|
284
|
-
return
|
|
281
|
+
io = env["rack.input"]
|
|
282
|
+
return({}) if io.nil?
|
|
285
283
|
|
|
286
284
|
# `rack.input` is normally a StringIO wrapping the body_binstr
|
|
287
285
|
# we staged in src/worker.mjs. Read the full body; it's already
|
|
@@ -290,14 +288,14 @@ module Cloudflare
|
|
|
290
288
|
if io.respond_to?(:rewind)
|
|
291
289
|
begin
|
|
292
290
|
io.rewind
|
|
293
|
-
rescue
|
|
291
|
+
rescue StandardError
|
|
294
292
|
# some stubs don't support rewind — ignore
|
|
295
293
|
end
|
|
296
294
|
end
|
|
297
|
-
body = io.respond_to?(:read) ? io.read.to_s :
|
|
295
|
+
body = io.respond_to?(:read) ? io.read.to_s : ""
|
|
298
296
|
|
|
299
297
|
parsed = parse(body, ct)
|
|
300
|
-
env[
|
|
298
|
+
env["cloudflare.multipart"] = parsed
|
|
301
299
|
parsed
|
|
302
300
|
end
|
|
303
301
|
end
|
|
@@ -314,15 +312,17 @@ end
|
|
|
314
312
|
# bespoke Cloudflare::Multipart parser for multipart requests, falling
|
|
315
313
|
# back to the gem implementation for everything else.
|
|
316
314
|
|
|
317
|
-
require
|
|
315
|
+
require "rack/request"
|
|
318
316
|
|
|
319
317
|
module Rack
|
|
320
318
|
class Request
|
|
321
|
-
|
|
319
|
+
unless method_defined?(:__homura_original_POST)
|
|
320
|
+
alias_method :__homura_original_POST, :POST
|
|
321
|
+
end
|
|
322
322
|
|
|
323
323
|
def POST
|
|
324
|
-
ct = env[
|
|
325
|
-
if ct && ct.to_s.downcase.include?(
|
|
324
|
+
ct = env["CONTENT_TYPE"]
|
|
325
|
+
if ct && ct.to_s.downcase.include?("multipart/")
|
|
326
326
|
::Cloudflare::Multipart.rack_params(env)
|
|
327
327
|
else
|
|
328
328
|
__homura_original_POST
|