Skip to content
File

Blob: src/workerd/api/worker-loader.c++

11.3 KB
1#include "worker-loader.h"
2 
3#include <workerd/api/actor.h>
4#include <workerd/api/http.h>
5#include <workerd/io/compatibility-date.h>
6#include <workerd/io/features.h>
7#include <workerd/io/io-context.h>
8 
9#include <capnp/message.h>
10 
11namespace workerd::api {
12 
13jsg::Ref<Fetcher> WorkerStub::getEntrypoint(jsg::Lock& js,
14 jsg::Optional<kj::Maybe<kj::String>> name,
15 jsg::Optional<EntrypointOptions> options) {
16 Frankenvalue props;
17 kj::Maybe<ResourceLimits> limits;
18 KJ_IF_SOME(o, options) {
19 KJ_IF_SOME(p, o.props) {
20 props = Frankenvalue::fromJs(js, p.getHandle(js));
21 }
22 limits = o.limits;
23 }
24 
25 kj::Maybe<kj::String> entrypointName;
26 KJ_IF_SOME(n, name) {
27 KJ_IF_SOME(n2, n) {
28 if (n2 != "default"_kj) {
29 entrypointName = kj::mv(n2);
30 }
31 }
32 }
33 
34 auto subreqChannel = channel->getEntrypoint(kj::mv(entrypointName), kj::mv(props), limits);
35 return js.alloc<Fetcher>(IoContext::current().addObject(kj::mv(subreqChannel)));
36}
37 
38jsg::Ref<DurableObjectClass> WorkerStub::getDurableObjectClass(jsg::Lock& js,
39 jsg::Optional<kj::Maybe<kj::String>> name,
40 jsg::Optional<EntrypointOptions> options) {
41 Frankenvalue props;
42 kj::Maybe<ResourceLimits> limits;
43 KJ_IF_SOME(o, options) {
44 KJ_IF_SOME(p, o.props) {
45 props = Frankenvalue::fromJs(js, p.getHandle(js));
46 }
47 limits = o.limits;
48 }
49 
50 kj::Maybe<kj::String> entrypointName;
51 KJ_IF_SOME(n, name) {
52 KJ_IF_SOME(n2, n) {
53 if (n2 != "default"_kj) {
54 entrypointName = kj::mv(n2);
55 }
56 }
57 }
58 
59 return js.alloc<DurableObjectClass>(IoContext::current().addObject(
60 channel->getActorClass(kj::mv(entrypointName), kj::mv(props), limits)));
61}
62 
63jsg::Ref<WorkerStub> WorkerLoader::get(
64 jsg::Lock& js, kj::Maybe<kj::String> name, jsg::Function<jsg::Promise<WorkerCode>()> getCode) {
65 auto& ioctx = IoContext::current();
66 
67 auto reenterAndGetCode = ioctx.makeReentryCallback(
68 [&ioctx, getCode = kj::mv(getCode), compatDateValidation = compatDateValidation](
69 jsg::Lock& js) mutable {
70 return getCode(js).then(
71 js, [&ioctx, compatDateValidation](jsg::Lock& js, WorkerCode code) -> DynamicWorkerSource {
72 return toDynamicWorkerSource(js, ioctx, compatDateValidation, kj::mv(code));
73 });
74 });
75 
76 auto isolateChannel =
77 ioctx.getIoChannelFactory().loadIsolate(channel, kj::mv(name), kj::mv(reenterAndGetCode));
78 
79 return js.alloc<WorkerStub>(ioctx.addObject(kj::mv(isolateChannel)));
80}
81 
82jsg::Ref<WorkerStub> WorkerLoader::load(jsg::Lock& js, WorkerCode code) {
83 auto& ioctx = IoContext::current();
84 
85 auto source = toDynamicWorkerSource(js, ioctx, compatDateValidation, kj::mv(code));
86 
87 // Annoyingly, the callback we pass to `loadIsolate()` technically may be called any number of
88 // times. Yes, even though we aren't providing an ID. The runtime can actually evict the isolate
89 // while a stub still exists, as long as there is no active request on the stub, and then
90 // recreate the isolate on the next request. Moreover, it may ultimately destroy the `ownContent`
91 // in another thread, so we need to use atomic refcounting on it. Ugh!
92 struct OwnContentWrapper: public kj::AtomicRefcounted {
93 kj::Own<void> content;
94 OwnContentWrapper(kj::Own<void> content): content(kj::mv(content)) {}
95 };
96 auto ownContentWrapper = kj::atomicRefcounted<OwnContentWrapper>(kj::mv(source.ownContent));
97 
98 auto isolateChannel = ioctx.getIoChannelFactory().loadIsolate(channel, kj::none,
99 [source = kj::mv(source), ownContentWrapper = kj::mv(ownContentWrapper)]() mutable {
100 return source.clone(kj::atomicAddRef(*ownContentWrapper));
101 });
102 
103 return js.alloc<WorkerStub>(ioctx.addObject(kj::mv(isolateChannel)));
104}
105 
106DynamicWorkerSource WorkerLoader::toDynamicWorkerSource(jsg::Lock& js,
107 IoContext& ioctx,
108 CompatibilityDateValidation compatDateValidation,
109 WorkerCode code) {
110 auto extractedSource = extractSource(js, code);
111 auto ownCompatFlags = extractCompatFlags(js, code, compatDateValidation);
112 CompatibilityFlags::Reader compatFlags = *ownCompatFlags;
113 
114 Frankenvalue env;
115 KJ_IF_SOME(codeEnv, code.env) {
116 env = Frankenvalue::fromJs(js, codeEnv.getHandle(js));
117 }
118 
119 kj::Maybe<kj::Own<IoChannelFactory::SubrequestChannel>> globalOutbound;
120 KJ_IF_SOME(maybeOut, code.globalOutbound) {
121 KJ_IF_SOME(out, maybeOut) {
122 auto channel = out->getSubrequestChannel(ioctx);
123 channel->requireAllowsTransfer();
124 globalOutbound = kj::mv(channel);
125 } else {
126 // Application passed `null` to disable internet access. Leave `globalOutbound` as
127 // `kj::none`.
128 }
129 } else {
130 // Inherit the calling worker's global outbound channel.
131 //
132 // Note we don't need to enforce transferrability in this case because if it was the global
133 // outbound of the parent, it must be OK to be the global outbound of the child.
134 globalOutbound =
135 ioctx.getIoChannelFactory().getSubrequestChannel(IoContext::NULL_CLIENT_CHANNEL);
136 }
137 
138 kj::Array<kj::Own<IoChannelFactory::SubrequestChannel>> tailChannels;
139 KJ_IF_SOME(tails, code.tails) {
140 tailChannels = KJ_MAP(tail, tails) {
141 auto channel = tail->getSubrequestChannel(ioctx);
142 channel->requireAllowsTransfer();
143 return kj::mv(channel);
144 };
145 }
146 
147 kj::Array<kj::Own<IoChannelFactory::SubrequestChannel>> streamingTailChannels;
148 KJ_IF_SOME(streamingTails, code.streamingTails) {
149 JSG_REQUIRE(code.allowExperimental.orDefault(false), Error,
150 "Streaming tail workers are experimental. You must pass the option "
151 "'allowExperimental: true' to the worker loader to use them");
152 
153 streamingTailChannels = KJ_MAP(tail, streamingTails) {
154 auto channel = tail->getSubrequestChannel(ioctx);
155 channel->requireAllowsTransfer();
156 return kj::mv(channel);
157 };
158 }
159 
160 return {.source = kj::mv(extractedSource),
161 .compatibilityFlags = compatFlags,
162 .limits = code.limits,
163 .env = kj::mv(env),
164 .globalOutbound = kj::mv(globalOutbound),
165 .tails = kj::mv(tailChannels),
166 .streamingTails = kj::mv(streamingTailChannels),
167 .ownContent = ownCompatFlags.attach(kj::mv(code.modules), kj::mv(code.mainModule)),
168 .ownContentIsRpcResponse = false};
169}
170 
171Worker::Script::Source WorkerLoader::extractSource(jsg::Lock& js, WorkerCode& code) {
172 JSG_REQUIRE(code.modules.fields.size() > 0, TypeError,
173 "Dynamic Worker code must contain at least one module.");
174 
175 auto modules = KJ_MAP(entry, code.modules.fields) -> Worker::Script::Module {
176 KJ_SWITCH_ONEOF(entry.value) {
177 KJ_CASE_ONEOF(text, kj::String) {
178 if (entry.name.endsWith(".py"_kj)) {
179 return {
180 .name = entry.name,
181 .content = Worker::Script::PythonModule{.body = text},
182 };
183 }
184 
185 if (entry.name.endsWith(".js"_kj)) {
186 return {
187 .name = entry.name,
188 .content = Worker::Script::EsModule{.body = text},
189 };
190 }
191 
192 if (entry.name.endsWith(".ts"_kj) || entry.name.endsWith(".tsx"_kj) ||
193 entry.name.endsWith(".jsx"_kj)) {
194 JSG_FAIL_REQUIRE(TypeError,
195 "Module name must end with '.js' or '.py' (or the content must be an object ",
196 "indicating the type explicitly). Got: ", entry.name,
197 ". If you're trying to load TypeScript, bundle it first with ",
198 "'@cloudflare/worker-bundler' and pass the generated JavaScript modules.");
199 }
200 
201 JSG_FAIL_REQUIRE(TypeError,
202 "Module name must end with '.js' or '.py' (or the content must be an object ",
203 "indicating the type explicitly). Got: ", entry.name);
204 }
205 KJ_CASE_ONEOF(module, Module) {
206 uint fieldCount = (module.js != kj::none) + (module.cjs != kj::none) +
207 (module.text != kj::none) + (module.data != kj::none) + (module.json != kj::none) +
208 (module.py != kj::none) + (module.wasm != kj::none);
209 JSG_REQUIRE(fieldCount == 1, TypeError,
210 "Each module must contain exactly one of 'js', 'cjs', 'text', 'data', 'json', 'py', or 'wasm'. "
211 "Module '",
212 entry.name, "' contained ", fieldCount, " properties.");
213 
214 return {.name = entry.name, .content = [&]() -> Worker::Script::ModuleContent {
215 KJ_IF_SOME(js, module.js) {
216 // TODO: this might need typescript transpilation too.
217 return Worker::Script::EsModule{.body = js};
218 } else KJ_IF_SOME(cjs, module.cjs) {
219 return Worker::Script::CommonJsModule{.body = cjs};
220 } else KJ_IF_SOME(text, module.text) {
221 return Worker::Script::TextModule{.body = text};
222 } else KJ_IF_SOME(data, module.data) {
223 return Worker::Script::DataModule{.body = data};
224 } else KJ_IF_SOME(json, module.json) {
225 kj::StringPtr serialized =
226 module.serializedJson.emplace(js.serializeJson(kj::mv(json)));
227 // We moved out of `json`, making it an empty V8Ref, explicitly
228 // clear out the field as we don't intend to re-use this
229 module.json = kj::none;
230 return Worker::Script::JsonModule{.body = serialized};
231 } else KJ_IF_SOME(py, module.py) {
232 return Worker::Script::PythonModule{.body = py};
233 } else KJ_IF_SOME(wasm, module.wasm) {
234 return Worker::Script::WasmModule{.body = wasm};
235 } else {
236 KJ_UNREACHABLE;
237 }
238 }()};
239 }
240 }
241 KJ_UNREACHABLE;
242 };
243 
244 bool isPython = code.mainModule.endsWith(".py"_kj);
245 // Disallow Python modules when the main module is a JS module, and vice versa.
246 for (auto& module: modules) {
247 auto isJsModule = module.content.is<Worker::Script::EsModule>() ||
248 module.content.is<Worker::Script::CommonJsModule>();
249 if (isPython && isJsModule) {
250 JSG_FAIL_REQUIRE(TypeError, "Module \"", module.name,
251 "\" is a JS module, but the main module is a Python module.");
252 }
253 auto isPythonModule = module.content.is<Worker::Script::PythonModule>();
254 if (!isPython && isPythonModule) {
255 JSG_FAIL_REQUIRE(TypeError, "Module \"", module.name,
256 "\" is a Python module, but the main module isn't a Python module.");
257 }
258 }
259 
260 return Worker::Script::ModulesSource{
261 .mainModule = code.mainModule,
262 .modules = kj::mv(modules),
263 .isPython = isPython,
264 };
265}
266 
267kj::Own<CompatibilityFlags::Reader> WorkerLoader::extractCompatFlags(
268 jsg::Lock& js, WorkerCode& code, CompatibilityDateValidation compatDateValidation) {
269 bool allowExperimental = code.allowExperimental.orDefault(false);
270 if (!FeatureFlags::get(js).getWorkerdExperimental()) {
271 JSG_REQUIRE(!allowExperimental, Error,
272 "'allowExperimental' is only allowed when the calling worker has the 'experimental' "
273 "compat flag set.");
274 }
275 
276 kj::ArrayPtr<const kj::String> compatFlags;
277 KJ_IF_SOME(f, code.compatibilityFlags) {
278 compatFlags = f;
279 }
280 
281 capnp::word scratch[capnp::sizeInWords<CompatibilityFlags>() + 4]{};
282 capnp::MallocMessageBuilder compatFlagsMessage(scratch);
283 auto compatFlagsBuilder = compatFlagsMessage.getRoot<CompatibilityFlags>();
284 
285 SimpleWorkerErrorReporter errorReporter;
286 
287 compileCompatibilityFlags(code.compatibilityDate, compatFlags, compatFlagsBuilder, errorReporter,
288 allowExperimental, compatDateValidation);
289 
290 if (!errorReporter.errors.empty()) {
291 JSG_FAIL_REQUIRE(Error, errorReporter.errors.front());
292 }
293 
294 return capnp::clone(compatFlagsBuilder.asReader());
295}
296 
297} // namespace workerd::api