File
Blob: src/workerd/api/worker-loader.c++
| 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 | |
| 11 | namespace workerd::api { |
| 12 | |
| 13 | jsg::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 | |
| 38 | jsg::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 | |
| 63 | jsg::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 | |
| 82 | jsg::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 | |
| 106 | DynamicWorkerSource 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 | |
| 171 | Worker::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 | |
| 267 | kj::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 |