├── Makefile ├── sandstorm-pkgdef.capnp ├── client └── index.html ├── README.md └── server.c++ /Makefile: -------------------------------------------------------------------------------- 1 | CXX=g++ 2 | CXXFLAGS=-O2 -Wall 3 | CXXFLAGS2=-std=c++1y -Itmp $(CXXFLAGS) 4 | SANDSTORM_CAPNP_DIR=/opt/sandstorm/latest/usr/include 5 | 6 | .PHONEY: all clean dev 7 | 8 | package.spk: server sandstorm-pkgdef.capnp 9 | spk pack package.spk 10 | 11 | dev: server sandstorm-pkgdef.capnp 12 | spk dev 13 | 14 | clean: 15 | rm -rf tmp server package.spk 16 | 17 | tmp/genfiles: 18 | @mkdir -p tmp 19 | capnp compile --src-prefix=$(SANDSTORM_CAPNP_DIR) -oc++:tmp $(SANDSTORM_CAPNP_DIR)/sandstorm/*.capnp 20 | @touch tmp/genfiles 21 | 22 | server: tmp/genfiles server.c++ 23 | $(CXX) -static server.c++ tmp/sandstorm/*.capnp.c++ -o server $(CXXFLAGS2) `pkg-config capnp-rpc --cflags --libs` 24 | -------------------------------------------------------------------------------- /sandstorm-pkgdef.capnp: -------------------------------------------------------------------------------- 1 | @0xeef286f78b0168e0; 2 | # When cloning the example, you'll want to replace the above file ID with a new 3 | # one generated using the `capnp id` command. 4 | 5 | using Spk = import "/sandstorm/package.capnp"; 6 | # This imports: 7 | # $SANDSTORM_HOME/latest/usr/include/sandstorm/package.capnp 8 | # Check out that file to see the full, documented package definition format. 9 | 10 | const pkgdef :Spk.PackageDefinition = ( 11 | # The package definition. Note that the spk tool looks specifically for the 12 | # "pkgdef" constant. 13 | 14 | id = "8e4wpgf664gqek74uy6cmq3kyw1xu0ztfj8ktwwa9yxv90ew24z0", 15 | # The app ID is actually the public key used to sign the app package. 16 | # All packages with the same ID are versions of the same app. 17 | # 18 | # If you are working from the example, you'll need to replace the above 19 | # public key with one of your own. Use the `spk keygen` command to generate 20 | # a new one. 21 | 22 | manifest = ( 23 | # This manifest is included in your app package to tell Sandstorm 24 | # about your app. 25 | 26 | appTitle = (defaultText = "Raw API Example"), 27 | 28 | appVersion = 0, # Increment this for every release. 29 | appMarketingVersion = (defaultText = "0.0.0"), 30 | 31 | actions = [ 32 | # Define your "new document" handlers here. 33 | ( title = (defaultText = "New Raw API Example Instance"), 34 | nounPhrase = (defaultText = "instance"), 35 | command = .myCommand 36 | # The command to run when starting for the first time. (".myCommand" 37 | # is just a constant defined at the bottom of the file.) 38 | ) 39 | ], 40 | 41 | continueCommand = .myCommand 42 | # This is the command called to start your app back up after it has been 43 | # shut down for inactivity. Here we're using the same command as for 44 | # starting a new instance, but you could use different commands for each 45 | # case. 46 | ), 47 | 48 | sourceMap = ( 49 | # Here we define where to look for files to copy into your package. 50 | searchPath = [ 51 | ( packagePath = "server", sourcePath = "server" ), 52 | # Map server binary at "/server". 53 | 54 | ( packagePath = "client", sourcePath = "client" ), 55 | # Map client directory at "/client". 56 | ] 57 | ), 58 | 59 | alwaysInclude = [ "." ] 60 | # Always include all mapped files, whether or not they are opened during 61 | # "spk dev". 62 | ); 63 | 64 | const myCommand :Spk.Manifest.Command = ( 65 | # Here we define the command used to start up your server. 66 | argv = ["/server"], 67 | environ = [ 68 | # Note that this defines the *entire* environment seen by your app. 69 | (key = "PATH", value = "/usr/local/bin:/usr/bin:/bin") 70 | ] 71 | ); 72 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sandstorm Raw API sample app 5 | 6 | 82 | 83 | 84 |

Hello World!

85 |

This is client/index.html. Go edit it and refresh! 86 |


98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sandstorm Raw API Example App 2 | 3 | This is an example [Sandstorm](https://sandstorm.io) application which uses the raw [Cap'n Proto](https://capnproto.org)-based Sandstorm API to serve a web UI without an HTTP server. Most apps instead use `sandstorm-http-bridge` wrapping a traditional server, but this one does't. 4 | 5 | ## Why do that? 6 | 7 | You might want to write a Sandstorm app using the raw API if: 8 | 9 | * You want to be as efficient as possible. Raw API apps -- especially if written in C++ or Rust -- take drastically less RAM, start much faster, and produce much smaller distributable packages. 10 | * You want to avoid legacy cruft. A traditional HTTP server is a huge, complicated piece of code much of which is not important for Sandstorm. 11 | * You want to use a language that has poor HTTP support, but good Cap'n Proto RPC support, such as C++ or Rust. 12 | 13 | That said, you should prefer `sandstorm-http-bridge` around a traditional HTTP server if: 14 | 15 | * You are porting an existing HTTP-based app. 16 | * You want to build on top of a standard HTTP server framework. 17 | * You want to use a programming language that doesn't have good Cap'n Proto RPC support. 18 | 19 | ## Instructions 20 | 21 | 1. Get a Linux machine. 22 | 2. Install [Cap'n Proto](https://capnproto.org). You'll need to use the latest code from git, not a release version, because Sandstorm and Cap'n Proto are developed together and Sandstorm uses unreleased features from Cap'n Proto. 23 | 3. Install [Sandstorm](https://github.com/sandstorm-io/sandstorm) and make sure it is successfully running locally. 24 | 4. Clone this git repo. 25 | 5. CD to the repo and `make dev`. 26 | 6. Open your local Sandstorm. The app should be visible there. 27 | 7. Ctrl+C in the terminal running `make dev` to disconnect. 28 | 29 | _Note: You can ignore the bogus warning about `getaddrinfo` and static linking -- the app will never actually call `getaddrinfo`. We statically link the binary so that there's no need to include shared libs in the package, making it smaller and simpler._ 30 | 31 | _Note: Due to an ongoing disagreement between [GCC's](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=58074) and [Clang's](https://llvm.org/bugs/show_bug.cgi?id=23764) interpretations of the C++ ABI, it is important that you use the same compiler to compile this example program as you use to compile Cap'n Proto. By default both will use GCC. You can run `make CXX=clang++` to build with Clang instead._ 32 | 33 | ## Building your own 34 | 35 | You can use this code as a starting point for your own app. 36 | 37 | 1. Edit `sandstorm-pkgdef.capnp`, read the comments, and edit. At the very least you want to rename the app and set a new App ID. 38 | 2. Place your own client code (HTML, CSS, Javascript, etc.) in the `client` directory. 39 | 3. Have your client code use HTTP GET, PUT, and DELETE requests to store user data under the HTTP path `/var`. You can create files under `/var` with any name, but you cannot create sub-directories (for now). 40 | 4. Use `make dev` to test. 41 | 5. Type just `make` to build a distributable package `package.spk`. 42 | 43 | Note that `server.c++` has some "features" that you may feel inclined to modify: 44 | 45 | * Only the owner of an app instance is allowed to write (i.e. issue PUT or DELETE requests under `/var`). Anyone can read (though they must of course first receive the secret URL from the owner). For many apps, it's more appropriate for everyone to have write access (again, provided they've received the URL). Search for `canWrite` in the code, and `getViewInfo`. 46 | * `Content-Type` headers are derived from file extensions, with only a small number of types supported. `index.html` is the default file for a directory. You can probably do better. 47 | * Various useful Sandstorm API features are not exposed. You could implement an HTTP API to get access to those features from your client app. 48 | 49 | ## Questions? 50 | 51 | [Ask on the dev group.](https://groups.google.com/group/sandstorm-dev) 52 | -------------------------------------------------------------------------------- /server.c++: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Sandstorm Development Group, Inc. 2 | // Licensed under the MIT License: 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | // Hack around stdlib bug with C++14. 23 | #include // force libstdc++ to include its config 24 | #undef _GLIBCXX_HAVE_GETS // correct broken config 25 | // End hack. 26 | 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | 42 | #include 43 | #include 44 | #include 45 | 46 | namespace { 47 | 48 | #if __QTCREATOR 49 | #define KJ_MVCAP(var) var 50 | // QtCreator dosen't understand C++14 syntax yet. 51 | #else 52 | #define KJ_MVCAP(var) var = ::kj::mv(var) 53 | // Capture the given variable by move. Place this in a lambda capture list. Requires C++14. 54 | // 55 | // TODO(cleanup): Move to libkj. 56 | #endif 57 | 58 | typedef unsigned int uint; 59 | typedef unsigned char byte; 60 | 61 | // ======================================================================================= 62 | // Utility functions 63 | // 64 | // Most of these should be moved to the KJ library someday. 65 | 66 | kj::AutoCloseFd createFile(kj::StringPtr name, int flags, mode_t mode = 0666) { 67 | // Create a file, returning an RAII wrapper around the file descriptor. Errors throw exceptinos. 68 | 69 | int fd; 70 | KJ_SYSCALL(fd = open(name.cStr(), O_CREAT | flags, mode), name); 71 | return kj::AutoCloseFd(fd); 72 | } 73 | 74 | size_t getFileSize(int fd, kj::StringPtr filename) { 75 | struct stat stats; 76 | KJ_SYSCALL(fstat(fd, &stats)); 77 | KJ_REQUIRE(S_ISREG(stats.st_mode), "Not a regular file.", filename); 78 | return stats.st_size; 79 | } 80 | 81 | kj::Maybe tryOpen(kj::StringPtr name, int flags, mode_t mode = 0666) { 82 | // Try to open a file, returning an RAII wrapper around the file descriptor, or null if the 83 | // file doesn't exist. All other errors throw exceptions. 84 | 85 | int fd; 86 | 87 | while ((fd = open(name.cStr(), flags, mode)) < 0) { 88 | int error = errno; 89 | if (error == ENOENT) { 90 | return nullptr; 91 | } else if (error != EINTR) { 92 | KJ_FAIL_SYSCALL("open(name)", error, name); 93 | } 94 | } 95 | 96 | return kj::AutoCloseFd(fd); 97 | } 98 | 99 | bool isDirectory(kj::StringPtr filename) { 100 | // Return true if the parameter names a directory, false if it's any other kind of node or 101 | // doesn't exist. 102 | 103 | struct stat stats; 104 | while (stat(filename.cStr(), &stats) < 0) { 105 | if (errno != EINTR) { 106 | return false; 107 | } 108 | } 109 | return S_ISDIR(stats.st_mode); 110 | } 111 | 112 | kj::Vector listDirectory(kj::StringPtr dirname) { 113 | // Return a list of all filenames in the given directory, except "." and "..". 114 | 115 | kj::Vector entries; 116 | 117 | DIR* dir = opendir(dirname.cStr()); 118 | if (dir == nullptr) { 119 | KJ_FAIL_SYSCALL("opendir", errno, dirname); 120 | } 121 | KJ_DEFER(closedir(dir)); 122 | 123 | for (;;) { 124 | errno = 0; 125 | struct dirent* entry = readdir(dir); 126 | if (entry == nullptr) { 127 | int error = errno; 128 | if (error == 0) { 129 | break; 130 | } else { 131 | KJ_FAIL_SYSCALL("readdir", error, dirname); 132 | } 133 | } 134 | 135 | kj::StringPtr name = entry->d_name; 136 | if (name != "." && name != "..") { 137 | entries.add(kj::heapString(entry->d_name)); 138 | } 139 | } 140 | 141 | return entries; 142 | } 143 | 144 | // ======================================================================================= 145 | // WebSession implementation (interface declared in sandstorm/web-session.capnp) 146 | 147 | class WebSessionImpl final: public sandstorm::WebSession::Server { 148 | public: 149 | WebSessionImpl(sandstorm::UserInfo::Reader userInfo, 150 | sandstorm::SessionContext::Client context, 151 | sandstorm::WebSession::Params::Reader params) { 152 | // Permission #0 is "write". Check if bit 0 in the PermissionSet is set. 153 | auto permissions = userInfo.getPermissions(); 154 | canWrite = permissions.size() > 0 && (permissions[0] & 1); 155 | 156 | // `UserInfo` is defined in `sandstorm/grain.capnp` and contains info like: 157 | // - A stable ID for the user, so you can correlate sessions from the same user. 158 | // - The user's display name, e.g. "Mark Miller", useful for identifying the user to other 159 | // users. 160 | // - The user's permissions (seen above). 161 | 162 | // `WebSession::Params` is defined in `sandstorm/web-session.capnp` and contains info like: 163 | // - The hostname where the grain was mapped for this user. Every time a user opens a grain, 164 | // it is mapped at a new random hostname for security reasons. 165 | // - The user's User-Agent and Accept-Languages headers. 166 | 167 | // `SessionContext` is defined in `sandstorm/grain.capnp` and implements callbacks for 168 | // sharing/access control and service publishing/discovery. 169 | } 170 | 171 | kj::Promise get(GetContext context) override { 172 | // HTTP GET request. 173 | 174 | auto path = context.getParams().getPath(); 175 | requireCanonicalPath(path); 176 | 177 | if (path == "var" || path == "var/") { 178 | // Return a listing of the directory contents, one per line. 179 | auto text = kj::strArray(listDirectory("var"), "\n"); 180 | auto response = context.getResults().initContent(); 181 | response.setMimeType("text/plain"); 182 | response.getBody().setBytes( 183 | kj::arrayPtr(reinterpret_cast(text.begin()), text.size())); 184 | return kj::READY_NOW; 185 | } else if (path.startsWith("var/")) { 186 | // Serve all files under /var with type application/octet-stream since it comes from the 187 | // user. E.g. serving as "text/html" here would allow someone to trivially XSS other users 188 | // of the grain by PUTing malicious HTML content. (Such an attack wouldn't be a huge deal: 189 | // it would only allow the attacker to hijack another user's access to this grain, not to 190 | // Sandstorm in general, and if they attacker already has write access to upload the 191 | // malicious content, they have little to gain from hijacking another session.) 192 | return readFile(path, context, "application/octet-stream"); 193 | } else if (path == ".can-write") { 194 | // Fetch "/.can-write" to determine if the user has write permission, so you can show them 195 | // a different UI if not. 196 | auto response = context.getResults().initContent(); 197 | response.setMimeType("text/plain"); 198 | response.getBody().setBytes(kj::str(canWrite).asBytes()); 199 | return kj::READY_NOW; 200 | } else if (path == "" || path.endsWith("/")) { 201 | // A directory. Serve "index.html". 202 | return readFile(kj::str("client/", path, "index.html"), context, "text/html; charset=UTF-8"); 203 | } else { 204 | // Request for a static file. Look for it under "client/". 205 | auto filename = kj::str("client/", path); 206 | 207 | // Check if it's a directory. 208 | if (isDirectory(filename)) { 209 | // It is. Return redirect to add '/'. 210 | auto redirect = context.getResults().initRedirect(); 211 | redirect.setIsPermanent(true); 212 | redirect.setSwitchToGet(true); 213 | redirect.setLocation(kj::str(path, '/')); 214 | return kj::READY_NOW; 215 | } 216 | 217 | // Regular file (or non-existent). 218 | return readFile(kj::mv(filename), context, inferContentType(path)); 219 | } 220 | } 221 | 222 | kj::Promise put(PutContext context) override { 223 | // HTTP PUT request. 224 | 225 | auto params = context.getParams(); 226 | auto path = params.getPath(); 227 | requireCanonicalPath(path); 228 | 229 | KJ_REQUIRE(path.startsWith("var/"), "PUT only supported under /var."); 230 | 231 | if (!canWrite) { 232 | context.getResults().initClientError() 233 | .setStatusCode(sandstorm::WebSession::Response::ClientErrorCode::FORBIDDEN); 234 | } else { 235 | auto tempPath = kj::str(path, ".uploading"); 236 | auto data = params.getContent().getContent(); 237 | 238 | kj::FdOutputStream(createFile(tempPath, O_WRONLY | O_TRUNC)) 239 | .write(data.begin(), data.size()); 240 | 241 | KJ_SYSCALL(rename(tempPath.cStr(), path.cStr())); 242 | context.getResults().initNoContent(); 243 | } 244 | 245 | return kj::READY_NOW; 246 | } 247 | 248 | kj::Promise delete_(DeleteContext context) override { 249 | // HTTP DELETE request. 250 | 251 | auto path = context.getParams().getPath(); 252 | requireCanonicalPath(path); 253 | 254 | KJ_REQUIRE(path.startsWith("var/"), "DELETE only supported under /var."); 255 | 256 | if (!canWrite) { 257 | context.getResults().initClientError() 258 | .setStatusCode(sandstorm::WebSession::Response::ClientErrorCode::FORBIDDEN); 259 | } else { 260 | while (unlink(path.cStr()) != 0) { 261 | int error = errno; 262 | if (error == ENOENT) { 263 | // Ignore file-not-found for idempotency. 264 | break; 265 | } else if (error != EINTR) { 266 | KJ_FAIL_SYSCALL("unlink", error); 267 | } 268 | } 269 | } 270 | 271 | return kj::READY_NOW; 272 | } 273 | 274 | private: 275 | bool canWrite; 276 | // True if the user has write permission. 277 | 278 | void requireCanonicalPath(kj::StringPtr path) { 279 | // Require that the path doesn't contain "." or ".." or consecutive slashes, to prevent path 280 | // injection attacks. 281 | // 282 | // Note that such attacks wouldn't actually accomplish much since everything outside /var 283 | // is a read-only filesystem anyway, containing the app package contents which are non-secret. 284 | 285 | KJ_REQUIRE(!path.startsWith("/")); 286 | KJ_REQUIRE(!path.startsWith("./") && path != "."); 287 | KJ_REQUIRE(!path.startsWith("../") && path != ".."); 288 | 289 | KJ_IF_MAYBE(slashPos, path.findFirst('/')) { 290 | requireCanonicalPath(path.slice(*slashPos + 1)); 291 | } 292 | } 293 | 294 | kj::StringPtr inferContentType(kj::StringPtr filename) { 295 | if (filename.endsWith(".html")) { 296 | return "text/html; charset=UTF-8"; 297 | } else if (filename.endsWith(".js")) { 298 | return "text/javascript; charset=UTF-8"; 299 | } else if (filename.endsWith(".css")) { 300 | return "text/css; charset=UTF-8"; 301 | } else if (filename.endsWith(".png")) { 302 | return "image/png"; 303 | } else if (filename.endsWith(".gif")) { 304 | return "image/gif"; 305 | } else if (filename.endsWith(".jpg") || filename.endsWith(".jpeg")) { 306 | return "image/jpeg"; 307 | } else if (filename.endsWith(".svg")) { 308 | return "image/svg+xml; charset=UTF-8"; 309 | } else if (filename.endsWith(".txt")) { 310 | return "text/plain; charset=UTF-8"; 311 | } else { 312 | return "application/octet-stream"; 313 | } 314 | } 315 | 316 | kj::Promise readFile( 317 | kj::StringPtr filename, GetContext context, kj::StringPtr contentType) { 318 | KJ_IF_MAYBE(fd, tryOpen(filename, O_RDONLY)) { 319 | auto size = getFileSize(*fd, filename); 320 | kj::FdInputStream stream(kj::mv(*fd)); 321 | auto response = context.getResults(capnp::MessageSize { size / sizeof(capnp::word) + 32, 0 }); 322 | auto content = response.initContent(); 323 | content.setStatusCode(sandstorm::WebSession::Response::SuccessCode::OK); 324 | content.setMimeType(contentType); 325 | stream.read(content.getBody().initBytes(size).begin(), size); 326 | return kj::READY_NOW; 327 | } else { 328 | auto error = context.getResults().initClientError(); 329 | error.setStatusCode(sandstorm::WebSession::Response::ClientErrorCode::NOT_FOUND); 330 | return kj::READY_NOW; 331 | } 332 | } 333 | }; 334 | 335 | // ======================================================================================= 336 | // UiView implementation (interface declared in sandstorm/grain.capnp) 337 | 338 | class UiViewImpl final: public sandstorm::UiView::Server { 339 | public: 340 | kj::Promise getViewInfo(GetViewInfoContext context) override { 341 | auto viewInfo = context.initResults(); 342 | 343 | // Define a "write" permission, and then define roles "editor" and "viewer" where only "editor" 344 | // has the "write" permission. This will allow people to share read-only. 345 | auto perms = viewInfo.initPermissions(1); 346 | auto write = perms[0]; 347 | write.setName("write"); 348 | write.initTitle().setDefaultText("write"); 349 | 350 | auto roles = viewInfo.initRoles(2); 351 | auto editor = roles[0]; 352 | editor.initTitle().setDefaultText("editor"); 353 | editor.initVerbPhrase().setDefaultText("can edit"); 354 | editor.initPermissions(1).set(0, true); // has "write" permission 355 | auto viewer = roles[1]; 356 | viewer.initTitle().setDefaultText("viewer"); 357 | viewer.initVerbPhrase().setDefaultText("can view"); 358 | viewer.initPermissions(1).set(0, false); // does not have "write" permission 359 | 360 | return kj::READY_NOW; 361 | } 362 | 363 | kj::Promise newSession(NewSessionContext context) override { 364 | auto params = context.getParams(); 365 | 366 | KJ_REQUIRE(params.getSessionType() == capnp::typeId(), 367 | "Unsupported session type."); 368 | 369 | context.getResults().setSession( 370 | kj::heap(params.getUserInfo(), params.getContext(), 371 | params.getSessionParams().getAs())); 372 | 373 | return kj::READY_NOW; 374 | } 375 | }; 376 | 377 | // ======================================================================================= 378 | // Program main 379 | 380 | class ServerMain { 381 | public: 382 | ServerMain(kj::ProcessContext& context): context(context), ioContext(kj::setupAsyncIo()) {} 383 | 384 | kj::MainFunc getMain() { 385 | return kj::MainBuilder(context, "Sandstorm Thin Server", 386 | "Intended to be run as the root process of a Sandstorm app.") 387 | .callAfterParsing(KJ_BIND_METHOD(*this, run)) 388 | .build(); 389 | } 390 | 391 | kj::MainBuilder::Validity run() { 392 | // Set up RPC on file descriptor 3. 393 | auto stream = ioContext.lowLevelProvider->wrapSocketFd(3); 394 | capnp::TwoPartyVatNetwork network(*stream, capnp::rpc::twoparty::Side::CLIENT); 395 | auto rpcSystem = capnp::makeRpcServer(network, kj::heap()); 396 | 397 | // The `CLIENT` side of a `capnp::TwoPartyVatNetwork` does not serve its bootstrap capability 398 | // until it has initiated a request for the bootstrap capability of the `SERVER` side. 399 | // Therefore, we need to restore the supervisor's `SandstormApi` capability, even if we are not 400 | // going to use it. 401 | { 402 | capnp::MallocMessageBuilder message; 403 | auto vatId = message.getRoot(); 404 | vatId.setSide(capnp::rpc::twoparty::Side::SERVER); 405 | sandstorm::SandstormApi<>::Client api = 406 | rpcSystem.bootstrap(vatId).castAs>(); 407 | } 408 | 409 | kj::NEVER_DONE.wait(ioContext.waitScope); 410 | } 411 | 412 | private: 413 | kj::ProcessContext& context; 414 | kj::AsyncIoContext ioContext; 415 | }; 416 | 417 | } // anonymous namespace 418 | 419 | KJ_MAIN(ServerMain) 420 | --------------------------------------------------------------------------------