Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/workerd/io/io-channels.h
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,11 @@ class IoChannelFactory: public virtual kj::Refcounted {
JSG_FAIL_REQUIRE(Error, "WorkerdDebugPort bindings are not supported by this runtime.");
}

// Get direct access to the current workerd process's debug port interface.
virtual rpc::WorkerdDebugPort::Client getWorkerdDebugPort() {
JSG_FAIL_REQUIRE(Error, "WorkerdDebugPort bindings are not supported by this runtime.");
}

// Converts a token created with {SubrequestChannel,ActorClassChannel}::getToken() back into a
// live channel. Default implementations throw.
virtual kj::Own<SubrequestChannel> subrequestChannelFromToken(
Expand Down
11 changes: 4 additions & 7 deletions src/workerd/server/server-test.c++
Original file line number Diff line number Diff line change
Expand Up @@ -6995,9 +6995,9 @@ KJ_TEST("Server: debug port RPC calls") {
}
}

KJ_TEST("Server: workerdDebugPort binding loopback test") {
// This test verifies that a worker can use the workerdDebugPort binding to connect
// back to the same workerd instance's debug port and access other services.
KJ_TEST("Server: workerdDebugPort binding current process test") {
// This test verifies that a worker can use the workerdDebugPort binding to access other services
// in the same process without opening a network connection.
TestServer test(R"((
services = [
( name = "target-service",
Expand Down Expand Up @@ -7029,8 +7029,7 @@ KJ_TEST("Server: workerdDebugPort binding loopback test") {
esModule =
`export default {
` async fetch(request, env, ctx) {
` // Connect to the debug port
` const client = await env.debugPort.connect("debug-addr");
` const client = env.debugPort.current();
`
` // Test 1: Access the default entrypoint
` const defaultFetcher = client.getEntrypoint("target-service");
Expand Down Expand Up @@ -7066,8 +7065,6 @@ KJ_TEST("Server: workerdDebugPort binding loopback test") {
]
))"_kj);

// Enable the debug port on a known address
test.server.enableDebugPort(kj::str("debug-addr"));
test.server.allowExperimental();

test.start();
Expand Down
225 changes: 117 additions & 108 deletions src/workerd/server/server.c++
Original file line number Diff line number Diff line change
Expand Up @@ -3387,6 +3387,7 @@ class Server::WorkerService final: public Service,
kj::Array<kj::Own<IoChannelFactory::SubrequestChannel>> streamingTails;
kj::Array<kj::Rc<WorkerLoaderNamespace>> workerLoaders;
kj::Maybe<kj::Network&> workerdDebugPortNetwork;
kj::Maybe<Server&> workerdDebugPortServer;
};
using LinkCallback =
kj::Function<LinkedIoChannels(WorkerService&, Worker::ValidationErrorReporter&)>;
Expand Down Expand Up @@ -4461,6 +4462,14 @@ class Server::WorkerService final: public Service,
"workerdDebugPort binding is not enabled for this worker");
}

rpc::WorkerdDebugPort::Client getWorkerdDebugPort() override {
auto& channels =
KJ_REQUIRE_NONNULL(ioChannels.tryGet<LinkedIoChannels>(), "link() has not been called");
return KJ_REQUIRE_NONNULL(
channels.workerdDebugPortServer, "workerdDebugPort binding is not enabled for this worker")
.makeWorkerdDebugPortClient();
}

kj::Own<SubrequestChannel> subrequestChannelFromToken(
ChannelTokenUsage usage, kj::ArrayPtr<const byte> token) override {
return channelTokenHandler.decodeSubrequestChannelToken(usage, token);
Expand Down Expand Up @@ -5998,6 +6007,7 @@ kj::Promise<kj::Own<Server::WorkerService>> Server::makeWorkerImpl(kj::StringPtr

if (def.hasWorkerdDebugPortBinding) {
result.workerdDebugPortNetwork = network;
result.workerdDebugPortServer = *this;
}

return result;
Expand Down Expand Up @@ -6633,135 +6643,134 @@ kj::Promise<void> Server::listenTcp(
// =======================================================================================
// Debug port for exposing all services via RPC

class Server::DebugPortListener {
class Server::WorkerdDebugPortImpl final: public rpc::WorkerdDebugPort::Server {
public:
DebugPortListener(Server& owner,
kj::Own<kj::ConnectionReceiver> listener,
capnp::HttpOverCapnpFactory& httpOverCapnpFactory)
: owner(owner),
listener(kj::mv(listener)),
WorkerdDebugPortImpl(
workerd::server::Server& srv, capnp::HttpOverCapnpFactory& httpOverCapnpFactory)
: srv(srv),
httpOverCapnpFactory(httpOverCapnpFactory) {}

kj::Promise<void> run() {
capnp::TwoPartyServer server(kj::heap<WorkerdDebugPortImpl>(&owner, httpOverCapnpFactory));
co_return co_await server.listen(*listener);
}
kj::Promise<void> getEntrypoint(GetEntrypointContext context) override {
auto params = context.getParams();
auto serviceName = params.getService();
auto propsReader = params.getProps();

private:
Server& owner;
kj::Own<kj::ConnectionReceiver> listener;
capnp::HttpOverCapnpFactory& httpOverCapnpFactory;
// Look up the service.
auto& serviceEntry = KJ_ASSERT_NONNULL(srv.services.find(serviceName),
kj::str("jsg.Error: Worker \"", serviceName, "\" not found"));
auto service = serviceEntry->service();

class WorkerdDebugPortImpl final: public rpc::WorkerdDebugPort::Server {
public:
WorkerdDebugPortImpl(
workerd::server::Server* srvPtr, capnp::HttpOverCapnpFactory& httpOverCapnpFactory)
: srv(*srvPtr),
httpOverCapnpFactory(httpOverCapnpFactory) {}

kj::Promise<void> getEntrypoint(GetEntrypointContext context) override {
auto params = context.getParams();
auto serviceName = params.getService();
auto propsReader = params.getProps();

// Look up the service.
auto& serviceEntry = KJ_ASSERT_NONNULL(srv.services.find(serviceName),
kj::str("jsg.Error: Worker \"", serviceName, "\" not found"));
auto service = serviceEntry->service();

// Convert props from Frankenvalue if provided
Frankenvalue props;
if (params.hasProps()) {
props = Frankenvalue::fromCapnp(propsReader);
}
// Convert props from Frankenvalue if provided
Frankenvalue props;
if (params.hasProps()) {
props = Frankenvalue::fromCapnp(propsReader);
}

kj::Own<Service> targetService;
kj::Own<Service> targetService;

// Try to cast to WorkerService to support entrypoints and props
KJ_IF_SOME(workerService, kj::tryDowncast<WorkerService>(*service)) {
// This is a WorkerService, use getEntrypoint which supports both entrypoints and props
kj::Maybe<kj::StringPtr> maybeEntrypoint;
if (params.hasEntrypoint()) {
maybeEntrypoint = params.getEntrypoint();
}
// Try to cast to WorkerService to support entrypoints and props
KJ_IF_SOME(workerService, kj::tryDowncast<WorkerService>(*service)) {
// This is a WorkerService, use getEntrypoint which supports both entrypoints and props
kj::Maybe<kj::StringPtr> maybeEntrypoint;
if (params.hasEntrypoint()) {
maybeEntrypoint = params.getEntrypoint();
}

targetService =
KJ_ASSERT_NONNULL(workerService.getEntrypoint(maybeEntrypoint, kj::mv(props)),
kj::str("jsg.Error: Worker does not export an entrypoint named \"",
maybeEntrypoint.orDefault("(default)"), "\""));
} else {
// Not a WorkerService
KJ_ASSERT(!params.hasEntrypoint(), "jsg.Error: Worker does not support named entrypoints");
targetService = KJ_ASSERT_NONNULL(workerService.getEntrypoint(maybeEntrypoint, kj::mv(props)),
kj::str("jsg.Error: Worker does not export an entrypoint named \"",
maybeEntrypoint.orDefault("(default)"), "\""));
} else {
// Not a WorkerService
KJ_ASSERT(!params.hasEntrypoint(), "jsg.Error: Worker does not support named entrypoints");

// Try to apply props if the service supports it
if (params.hasProps()) {
targetService = service->forProps(kj::mv(props), Persistent::NO);
} else {
// No props, just use the service as-is
targetService = kj::addRef(*service);
}
// Try to apply props if the service supports it
if (params.hasProps()) {
targetService = service->forProps(kj::mv(props), Persistent::NO);
} else {
// No props, just use the service as-is
targetService = kj::addRef(*service);
}

// Return a WorkerdBootstrap that wraps this service using the generic implementation.
context.initResults(capnp::MessageSize{4, 1})
.setEntrypoint(
kj::heap<WorkerdBootstrapImpl>(kj::mv(targetService), httpOverCapnpFactory));
return kj::READY_NOW;
}

kj::Promise<void> getActor(GetActorContext context) override {
auto params = context.getParams();
auto serviceName = params.getService();
auto entrypointName = params.getEntrypoint();
auto actorIdStr = params.getActorId();
// Return a WorkerdBootstrap that wraps this service using the generic implementation.
context.initResults(capnp::MessageSize{4, 1})
.setEntrypoint(kj::heap<WorkerdBootstrapImpl>(kj::mv(targetService), httpOverCapnpFactory));
return kj::READY_NOW;
}

// Look up the service
auto& serviceEntry = KJ_ASSERT_NONNULL(srv.services.find(serviceName),
kj::str("jsg.Error: Worker \"", serviceName, "\" not found"));
auto service = serviceEntry->service();
kj::Promise<void> getActor(GetActorContext context) override {
auto params = context.getParams();
auto serviceName = params.getService();
auto entrypointName = params.getEntrypoint();
auto actorIdStr = params.getActorId();

// Look up the service
auto& serviceEntry = KJ_ASSERT_NONNULL(srv.services.find(serviceName),
kj::str("jsg.Error: Worker \"", serviceName, "\" not found"));
auto service = serviceEntry->service();

// Try to cast to WorkerService
auto& workerService = KJ_REQUIRE_NONNULL(kj::tryDowncast<WorkerService>(*service),
"jsg.Error: Worker does not support Durable Objects");

// Look up the actor namespace
auto& actorNamespace = KJ_ASSERT_NONNULL(workerService.getActorNamespace(entrypointName),
kj::str("jsg.Error: Worker does not export a Durable Object class named \"", entrypointName,
"\""));

// Create an actor ID - use the namespace config to determine if it's durable or ephemeral
Worker::Actor::Id actorId;
KJ_SWITCH_ONEOF(actorNamespace.getConfig()) {
KJ_CASE_ONEOF(c, Durable) {
// Durable Object ID (hex-encoded SHA256 hash)
auto decoded = kj::decodeHex(actorIdStr);
KJ_REQUIRE(decoded.size() == SHA256_DIGEST_LENGTH,
"Invalid Durable Object ID: expected 64 hex characters (32 bytes)", decoded.size());
kj::Own<ActorIdFactory::ActorId> id =
kj::heap<ActorIdFactoryImpl::ActorIdImpl>(decoded.begin(), kj::none);
actorId = kj::mv(id);
}
KJ_CASE_ONEOF(c, Ephemeral) {
// Ephemeral actor ID (plain string)
actorId = kj::str(actorIdStr);
}
}

// Try to cast to WorkerService
auto& workerService = KJ_REQUIRE_NONNULL(kj::tryDowncast<WorkerService>(*service),
"jsg.Error: Worker does not support Durable Objects");
// Wrap the actor channel using the generic WorkerdBootstrap implementation.
context.initResults(capnp::MessageSize{4, 1})
.setActor(kj::heap<WorkerdBootstrapImpl>(
actorNamespace.getActorChannel(kj::mv(actorId)), httpOverCapnpFactory));
return kj::READY_NOW;
}

// Look up the actor namespace
auto& actorNamespace = KJ_ASSERT_NONNULL(workerService.getActorNamespace(entrypointName),
kj::str("jsg.Error: Worker does not export a Durable Object class named \"",
entrypointName, "\""));
private:
workerd::server::Server& srv;
capnp::HttpOverCapnpFactory& httpOverCapnpFactory;
};

// Create an actor ID - use the namespace config to determine if it's durable or ephemeral
Worker::Actor::Id actorId;
KJ_SWITCH_ONEOF(actorNamespace.getConfig()) {
KJ_CASE_ONEOF(c, Durable) {
// Durable Object ID (hex-encoded SHA256 hash)
auto decoded = kj::decodeHex(actorIdStr);
KJ_REQUIRE(decoded.size() == SHA256_DIGEST_LENGTH,
"Invalid Durable Object ID: expected 64 hex characters (32 bytes)", decoded.size());
kj::Own<ActorIdFactory::ActorId> id =
kj::heap<ActorIdFactoryImpl::ActorIdImpl>(decoded.begin(), kj::none);
actorId = kj::mv(id);
}
KJ_CASE_ONEOF(c, Ephemeral) {
// Ephemeral actor ID (plain string)
actorId = kj::str(actorIdStr);
}
}
class Server::DebugPortListener {
public:
DebugPortListener(Server& owner, kj::Own<kj::ConnectionReceiver> listener)
: owner(owner),
listener(kj::mv(listener)) {}

// Wrap the actor channel using the generic WorkerdBootstrap implementation.
context.initResults(capnp::MessageSize{4, 1})
.setActor(kj::heap<WorkerdBootstrapImpl>(
actorNamespace.getActorChannel(kj::mv(actorId)), httpOverCapnpFactory));
return kj::READY_NOW;
}
kj::Promise<void> run() {
capnp::TwoPartyServer server(owner.makeWorkerdDebugPortClient());
co_return co_await server.listen(*listener);
}

private:
workerd::server::Server& srv;
capnp::HttpOverCapnpFactory& httpOverCapnpFactory;
};
private:
Server& owner;
kj::Own<kj::ConnectionReceiver> listener;
};

rpc::WorkerdDebugPort::Client Server::makeWorkerdDebugPortClient() {
return rpc::WorkerdDebugPort::Client(
kj::heap<WorkerdDebugPortImpl>(*this, globalContext->httpOverCapnpFactory));
}

kj::Promise<void> Server::listenDebugPort(kj::Own<kj::ConnectionReceiver> listener) {
DebugPortListener obj(*this, kj::mv(listener), globalContext->httpOverCapnpFactory);
DebugPortListener obj(*this, kj::mv(listener));
co_return co_await obj.run();
}

Expand Down
2 changes: 2 additions & 0 deletions src/workerd/server/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ class Server final: private kj::TaskSet::ErrorHandler, private ChannelTokenHandl
kj::Own<kj::ConnectionReceiver> listener, kj::Own<Service> service, kj::StringPtr addrStr);

kj::Promise<void> listenDebugPort(kj::Own<kj::ConnectionReceiver> listener);
rpc::WorkerdDebugPort::Client makeWorkerdDebugPortClient();

class InvalidConfigService;
class InvalidConfigActorClass;
Expand All @@ -318,6 +319,7 @@ class Server final: private kj::TaskSet::ErrorHandler, private ChannelTokenHandl
class HttpListener;
class TcpListener;
class DebugPortListener;
class WorkerdDebugPortImpl;

struct ErrorReporter;
struct ConfigErrorReporter;
Expand Down
7 changes: 7 additions & 0 deletions src/workerd/server/workerd-debug-port-client.c++
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,11 @@ jsg::Ref<WorkerdDebugPortClient> WorkerdDebugPortConnector::connect(
return js.alloc<WorkerdDebugPortClient>(context.addObject(kj::mv(state)));
}

jsg::Ref<WorkerdDebugPortClient> WorkerdDebugPortConnector::current(jsg::Lock& js) {
auto& context = IoContext::current();
auto state =
kj::refcounted<DebugPortConnectionState>(context.getIoChannelFactory().getWorkerdDebugPort());
return js.alloc<WorkerdDebugPortClient>(context.addObject(kj::mv(state)));
}

} // namespace workerd::server
17 changes: 11 additions & 6 deletions src/workerd/server/workerd-debug-port-client.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ class Fetcher;

namespace workerd::server {

// Holds the I/O state for a debug port connection: the TCP stream, capnp RPC client,
// and debug port capability. Refcounted to support deferred proxying - response bodies
// and WebSockets are proxied through the capnp connection, so it must stay alive until
// they're fully consumed. See WorkerdBootstrapSubrequestChannel::startRequest().
// Holds the I/O state for a debug port client. Refcounted to support deferred proxying - response
// bodies and WebSockets may use the capability after the originating request has completed.
class DebugPortConnectionState: public kj::Refcounted {
public:
explicit DebugPortConnectionState(rpc::WorkerdDebugPort::Client debugPort)
: debugPort(kj::mv(debugPort)) {}

DebugPortConnectionState(kj::Own<kj::AsyncIoStream> connection,
kj::Own<capnp::TwoPartyClient> rpcClient,
rpc::WorkerdDebugPort::Client debugPort)
Expand All @@ -34,8 +35,8 @@ class DebugPortConnectionState: public kj::Refcounted {
return kj::addRef(*this);
}

kj::Own<kj::AsyncIoStream> connection;
kj::Own<capnp::TwoPartyClient> rpcClient;
kj::Maybe<kj::Own<kj::AsyncIoStream>> connection;
kj::Maybe<kj::Own<capnp::TwoPartyClient>> rpcClient;
rpc::WorkerdDebugPort::Client debugPort;
};

Expand Down Expand Up @@ -105,8 +106,12 @@ class WorkerdDebugPortConnector: public jsg::Object {
// @returns A WorkerdDebugPortClient that lazily connects on first use
jsg::Ref<WorkerdDebugPortClient> connect(jsg::Lock& js, kj::String address);

// Access the current workerd process without opening a network connection.
jsg::Ref<WorkerdDebugPortClient> current(jsg::Lock& js);

JSG_RESOURCE_TYPE(WorkerdDebugPortConnector) {
JSG_METHOD(connect);
JSG_METHOD(current);
}
};

Expand Down
Loading