├── .github └── workflows │ ├── release.yml │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE.txt ├── README.md ├── examples ├── .assets │ ├── act.nk │ ├── as-actor.wasm │ ├── echo.wasm │ ├── echo2.wasm │ ├── extras.wasm │ ├── kvcounter.wasm │ ├── kvcounter_tweaked.wasm │ ├── libkeyvalue.so │ ├── libtest_streams_provider.so │ ├── libwascc_fs.so │ ├── libwascc_httpsrv.so │ ├── libwascc_logging.so │ ├── libwascc_nats.so │ ├── libwascc_redis.so │ ├── libwascc_streams_redis.so │ ├── logger.wasm │ ├── mod1.nk │ ├── mod2.nk │ ├── mod_extras.nk │ ├── mod_kvcounter.nk │ ├── mod_sub.nk │ ├── mod_sub2.nk │ ├── multibinding.wasm │ ├── subscriber.wasm │ ├── subscriber2.wasm │ ├── wasi_consumer.wasm │ └── wasi_provider.wasm ├── add_remove.rs ├── assemblyscript.rs ├── echo_middleware.rs ├── echoserver.rs ├── extras.rs ├── kvcounter.rs ├── kvcounter_manifest.rs ├── lattice │ ├── host1.yaml │ ├── host2.yaml │ └── host3.yaml ├── logger.rs ├── named_bindings.rs ├── oci_test.yaml ├── portable_provider.rs ├── prometheus_middleware.rs ├── replace.rs ├── replace_ui │ ├── .gitignore │ ├── README.md │ ├── favicon.ico │ ├── manifest.json │ ├── mime.types │ ├── nginx.template │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── index.js │ │ └── style.css │ └── start.sh ├── sample_manifest.yaml ├── start_stop.rs └── subscriber.rs ├── src ├── actor.rs ├── authz.rs ├── bin.rs ├── bus │ ├── inproc.rs │ ├── lattice.rs │ └── mod.rs ├── capability.rs ├── dispatch.rs ├── errors.rs ├── extras.rs ├── inthost.rs ├── lib.rs ├── manifest.rs ├── middleware │ ├── mod.rs │ └── prometheus.rs ├── plugins.rs └── spawns.rs └── tests ├── auth.rs ├── common.rs ├── core.rs ├── lattice.rs ├── lib.rs └── load.rs /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Create Release 13 | id: create_release 14 | uses: actions/create-release@v1 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | with: 18 | tag_name: ${{ github.ref }} 19 | release_name: Release ${{ github.ref }} 20 | draft: false 21 | prerelease: true 22 | - name: Output Release URL File 23 | run: echo "${{ steps.create_release.outputs.upload_url }}" > release_url.txt 24 | - name: Save Release URL File for publish 25 | uses: actions/upload-artifact@v1 26 | with: 27 | name: release_url 28 | path: release_url.txt 29 | 30 | publish: 31 | needs: release 32 | strategy: 33 | matrix: 34 | os: [ubuntu-latest, macos-latest, windows-latest] 35 | engine: [wasm3, wasmtime] 36 | 37 | runs-on: ${{ matrix.os }} 38 | 39 | steps: 40 | - name: Checkout 41 | uses: actions/checkout@v1 42 | - name: Get the version 43 | id: get_version 44 | shell: bash 45 | run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/} 46 | - name: Load Release URL File from release job 47 | uses: actions/download-artifact@v1 48 | with: 49 | name: release_url 50 | - name: Install clang on Windows 51 | if: ${{ startsWith(matrix.os, 'windows') }} 52 | run: | 53 | curl -fsSL -o LLVM9.exe https://releases.llvm.org/9.0.0/LLVM-9.0.0-win64.exe 54 | 7z x LLVM9.exe -y -o"C:/Program Files/LLVM" 55 | - name: Create release Build 56 | run: cargo build --features "bin manifest lattice ${{ matrix.engine }}" --release 57 | - name: Install 7Zip PowerShell Module (win) 58 | if: ${{ startsWith(matrix.os, 'windows') }} 59 | shell: powershell 60 | run: Install-Module 7Zip4PowerShell -Force -Verbose 61 | - name: Create Release Zip (windows) 62 | if: ${{ startsWith(matrix.os, 'windows') }} 63 | run: powershell Compress-7Zip "target\release\wascc-host.exe" -ArchiveFileName "wascchost-${{matrix.os}}.zip" -Format Zip 64 | - name: Create Release Zip (ubuntu) 65 | if: ${{ startsWith(matrix.os, 'ubuntu') }} 66 | run: zip -j wascchost-${{ matrix.os }}.zip ./target/release/wascc-host 67 | - name: Create Release Zip (mac) 68 | if: ${{ startsWith(matrix.os, 'mac') }} 69 | run: zip -j wascchost-${{ matrix.os }}.zip ./target/release/wascc-host 70 | 71 | - name: Get Release File Name & Upload URL 72 | id: get_release_info 73 | shell: bash 74 | run: | 75 | value=`cat release_url/release_url.txt` 76 | echo ::set-output name=upload_url::$value 77 | - name: Set friendly OS label (ubuntu) 78 | if: ${{ startsWith(matrix.os, 'ubuntu') }} 79 | run: echo ::set-env name=OS_LABEL::linux 80 | - name: Set friendly OS label (mac) 81 | if: ${{ startsWith(matrix.os, 'mac') }} 82 | run: echo ::set-env name=OS_LABEL::macos 83 | - name: Set friendly OS label (windows) 84 | shell: bash 85 | if: ${{ startsWith(matrix.os, 'windows') }} 86 | run: echo ::set-env name=OS_LABEL::windows 87 | - name: Upload Release Asset 88 | id: upload-release-asset 89 | uses: actions/upload-release-asset@v1 90 | env: 91 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 92 | with: 93 | upload_url: ${{ steps.get_release_info.outputs.upload_url }} 94 | asset_path: ./wascchost-${{ matrix.os }}.zip 95 | asset_name: wascchost-${{ steps.get_version.outputs.VERSION }}-${{ env.OS_LABEL }}-${{ matrix.engine }}-x86_64.zip 96 | asset_content_type: application/zip 97 | 98 | 99 | crates: 100 | needs: publish 101 | runs-on: ubuntu-latest 102 | steps: 103 | - name: Checkout 104 | uses: actions/checkout@v1 105 | - name: Cargo login 106 | env: 107 | CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} 108 | run: cargo login ${{ env.CRATES_TOKEN }} 109 | - name: Cargo publish 110 | run: cargo publish --no-verify 111 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | name: Build all features 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Run fmt 17 | run: cargo fmt -- --check 18 | - name: Build 19 | run: cargo build --verbose --all-features 20 | 21 | test_no_nats: 22 | name: Test without lattice and NATS 23 | runs-on: ubuntu-latest 24 | strategy: 25 | matrix: 26 | engine: ["wasm3", "wasmtime"] 27 | 28 | services: 29 | redis: 30 | image: redis 31 | ports: 32 | - 6379:6379 33 | steps: 34 | - uses: actions/checkout@v2 35 | - name: Run tests (no lattice no NATS) 36 | run: cargo test --features "bin ${{ matrix.engine }}" -- --test-threads=1 37 | 38 | test: 39 | name: Test feature matrix 40 | runs-on: ubuntu-latest 41 | strategy: 42 | matrix: 43 | engine: ["wasm3", "wasmtime"] 44 | 45 | services: 46 | nats: 47 | image: nats 48 | ports: 49 | - 6222:6222 50 | - 4222:4222 51 | - 8222:8222 52 | 53 | redis: 54 | image: redis 55 | ports: 56 | - 6379:6379 57 | 58 | steps: 59 | - uses: actions/checkout@v2 60 | - name: Run tests (featureless) 61 | run: cargo test --features "bin ${{ matrix.engine }}" -- --test-threads=1 62 | - name: Run tests (Everything but Lattice) 63 | run: cargo test --features "manifest bin ${{ matrix.engine }}" -- --test-threads=1 64 | - name: Run tests (manifest only) 65 | run: cargo test --features "manifest ${{ matrix.engine }}" -- --test-threads=1 66 | - name: Run tests (lattice mode) 67 | run: cargo test --features "lattice bin manifest ${{ matrix.engine }}" --test integration -- --test-threads=1 68 | env: 69 | LATTICE_HOST: 0.0.0.0 70 | LATTICE_RPC_TIMEOUT_MILLIS: 100 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | .idea 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # waSCC Host Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | _The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)_ 7 | 8 | ## [0.14.0] - 2020 OCT 30 9 | 10 | This version corresponds to the project milestone [0.14](https://github.com/wascc/wascc-host/milestone/3) 11 | 12 | ### Added 13 | 14 | * Host runtime will now look at the `OCI_REGISTRY_USER` and `OCI_REGISTRY_PASSWORD` environment variables whenever it needs to contact an OCI registry. 15 | * Added the `add_native_capability_from_registry` to download and load an OS/CPU arch appropriate provider plugin. 16 | 17 | ### Changed 18 | 19 | * All non-local references to capability providers and actors are now assumed to be registry references that can be satisfied by the indicated OCI registry. If basic authentication is required by that registry, then you can supply those credentials via the `OCI_REGISTRY_USER` and `OCI_REGISTRY_PASSWORD` environment variables. 20 | * The `add_actor_from_gantry` function has been renamed to `add_actor_from_registry` 21 | * Manifest files will now load either actors (if the actor string exists as a file) or registry-hosted modules (if the actor string is a registry reference) 22 | 23 | ### Caution 24 | 25 | The ability to remotely schedule capability providers in this version should be considered unstable, to be 26 | stabilized in the 0.15.0 release (the "async rewrite"), which will include a rework of how capability providers 27 | internally subscribe to the message bus and identify themselves to the host and lattice. 28 | 29 | ## [0.13.0] - 2020 SEP 30 30 | 31 | This version corresponds to the project milestone [0.13](https://github.com/wascc/wascc-host/milestone/2) 32 | 33 | ### Added 34 | 35 | * _Lattice Control Plane Protocol_ - A new protocol is now supported on the lattice to allow for "auctions" to take place to gather a list of hosts willing to launch a given actor. An auction request is sent out with a set of constraints, any host that can satisfy those contraints responds affirmatively to the auction, and then the conductor of the auction chooses from among the responding hosts and sends a command to that specific host telling it to launch the actor in question. Launching that actor is then done by downloading the actor's bytes from a [Gantry](https://github.com/wascc/gantry) server and running it. You can specify a set of key/value pairs that define constraints that are matched against host labels to definite affinity-style scheduling rules. 36 | * _Sim Gantry_ - The gantry repo now has a "sim gantry" binary that developers can use to instantly start a Gantry host atop an arbitrary directory that contains signed actor modules. 37 | * _Lattice-Scoped Bindings_ - In lattice mode, calling `set_binding` will tell all running instances of a given capability provider to provision resources for the given actor+configuration. If a capability provider is added to a completely empty host, it will re-establish its bindings (including provisioning resources) by querying the lattice. If all instances of that provider are shut down, then the lattice will "forget" those bindings. Actors can be added and removed without the need to re-bind them to their capability providers. 38 | * There is now a `remove_binding` function on the `Host` struct that will tell all running instances of a given capability provider to de-provision resources for a given actor group. 39 | * waPC now supports the choice of multiple underlying WebAssembly engines or drivers, so waSCC host now supports them through the use of feature flags. You can now choose your engine between `wasmtime` (the default) and `wasm3`. For more information on the difference, please see the driver repository in the [waPC](https://github.com/wapc) Github organization. 40 | 41 | ### Changed 42 | 43 | * The `bind_actor` function has been renamed to `set_binding` to better provide context around the distributed, idempotent nature of bindings. 44 | * You can no longer invoke `configure_gantry` on a running host. The gantry client can only be supplied by the host builder now, further improving the runtime security and reliability of the host. 45 | * The `gantry` feature has been merged with the `lattice` feature. Gantry (client) support is no longer an isolated opt-in feature because both require the NATS connection in order to function and it didn't make sense to communicate with Gantry while not having the ability to connect to a lattice. 46 | * Renamed `from_bytes` on the `Actor` struct to `from_slice`, and it now takes a `&[u8]` parameter instead of an owned vector. 47 | * Loading an actor from gantry via the host API call now requires a revision number (0 indicates pull the latest) 48 | * The Gantry client API to download an actor no longer requires a callback, it simply returns a vector of bytes. 49 | 50 | ### Removed 51 | 52 | * You can no longer call `set_label` on a running host. Labels _must_ be set with a `HostBuilder` 53 | 54 | ## [0.12.0] - 2020 AUG 30 55 | 56 | This version corresponds to the project milestone 0.12 57 | 58 | ### Added 59 | 60 | * _Host Labels_ - You can now add arbitrary key/value pairs to a host manifest (when feature enabled) or via the `set_label` function (when `lattice` is enabled). These labels are discoverable via lattice host probe query. The host runtime will automatically add the appropriate values for the following reserved labels which cannot be overridden: 61 | 62 | * hostcore.os 63 | * hostcore.osfamily 64 | * hostcore.arch 65 | 66 | * _Uptime_ - Uptime is now being tracked when the `lattice` feature is enabled, and will be reported in response to host probes on the control subject. 67 | * _Bus Events_ - When compiled in lattice mode, the waSCC host will emit events on the lattice that allow changes in state (e.g. actor start, stop, provider load, unload, etc) to be monitored either directly via NATS or with a command-line utility like `latticectl` 68 | * _Lattice Namespaces_ - You can now provide a namespace as a unit of isolation. This allows you to run multiple hosts on multiple isolated lattices all on top of the same NATS infrastructure. This can be done through the `LATTICE_NAMESPACE` environment variable or explicitly through the new `HostBuilder` 69 | * _HostBuilder_ - A new builder syntax is available to more fluently configure a new host. 70 | 71 | ### Changed 72 | 73 | * The `wascc_host` binary will now default its log level to `INFO`, and you can override this behavior with the standard `RUST_LOG` environment variable syntax. 74 | * The crate's `Error` type now requires `Send` and `Sync`. This should have very little impact on consumers. 75 | * The `WasccHost` struct has been renamed to `Host`. Per Rust style guidelines, structs should not be prefixed with their module names. 76 | 77 | ## [0.11.0] - 2020 JUL 24 78 | 79 | This version lays the groundwork for providing more advanced and pluggable authorization functionality in the future. 80 | 81 | ### Removed 82 | 83 | * The `set_auth_hook` function has been removed from the `WasccHost` struct 84 | 85 | ### Added 86 | 87 | * The `with_authorizer` function has been added to `WasccHost` to allow a developer to create an instance of a waSCC host with a custom authorizer (anything that implements the new `Authorizer` trait). While waSCC host will _always_ enforce the core capability claims when validating whether an actor can communicate with a given capability provider, the new `Authorizer` allows developers to build custom code that further constrains / limits the authorization when loading actors into a host and when actors are attempting to invoke operations against other actors or capability providers. 88 | 89 | ## [0.10.0] - 2020 JUL 9 90 | 91 | This release includes several lattice-related enhancements as well as some security and stability improvements. 92 | 93 | ### Changed 94 | 95 | * The `Invocation` type now includes its own set of claims that must be verified by receiving code. This prevents invocations from being forged on the wire in the case of intrusion. 96 | * The `InvocationTarget` enum has been renamed to `WasccEntity` to better clarify the expected communications patterns 97 | * Middleware now has the ability to indicate a stop or a short-circuit in the middleware change. The trait signature for middleware has changed and any middleware structs built against 0.9.0 will have to be upgraded. 98 | 99 | ### Added 100 | 101 | * Each waSCC host instance now generates its own unique signing key (of type server, nkey prefix is `N` for "node"). This signing key is used to mint forge-proof invocations for transmission over the lattice. 102 | * The host now announces (at `info` level) to the stdout log its version number. 103 | * All waSCC hosts in lattice mode will now perform an antiforgery check on inbound invocations. 104 | * All waSCC hosts in lattice mode will now respond to inventory requests allowing authorized clients to probe the lattice for actors, capabilities, bindings, and hosts. 105 | * The waSCC host will now supply a number of additional actor claims (name, capabilities, tags, expiration, and issuer) to the capability provider during the binding in the form of custom key-value pairs added to the configuration hash map. For the list of these new keys, see [waSCC Codec](../wascc-codec). 106 | 107 | ## [0.8.0] - 2020 JUN 8 108 | 109 | This release was primarily to accomodate the upgrade to the newest version of the [waSCC Codec](../wascc-codec). 110 | 111 | ### Changed 112 | 113 | All capability providers (including _portable_ WASI providers) are now required to respond to the operation `OP_GET_CAPABILITY_DESCRIPTOR` and return a messagepack-serialized struct containing metadata about the capability provider. This metadata includes: 114 | 115 | * Name 116 | * Documentation description 117 | * Version (semver string) and Revision (monotonic) 118 | * List of supported operations 119 | 120 | We created a simple _builder_ syntax that makes it easy and readable for capability providers to supply a capability descriptor: 121 | 122 | ```rust 123 | /// Obtains the capability provider descriptor 124 | fn get_descriptor(&self) -> Result, Box> { 125 | Ok(serialize( 126 | CapabilityDescriptor::builder() 127 | .id(CAPABILITY_ID) 128 | .name("Default waSCC HTTP Server Provider (Actix)") 129 | .long_description("A fast, multi-threaded HTTP server for waSCC actors") 130 | .version(VERSION) 131 | .revision(REVISION) 132 | .with_operation( 133 | OP_HANDLE_REQUEST, 134 | OperationDirection::ToActor, 135 | "Delivers an HTTP request to an actor and expects an HTTP response in return", 136 | ) 137 | .build(), 138 | )?) 139 | } 140 | ``` 141 | 142 | **NOTE** - This is a breaking change, so old versions of capability providers will _not_ work with this version of the waSCC host. 143 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wascc-host" 3 | version = "0.14.0" 4 | authors = ["Kevin Hoffman "] 5 | edition = "2018" 6 | homepage = "https://wascc.dev" 7 | repository = "https://github.com/wascc/wascc-host" 8 | description = "WebAssembly Secure Capabilities Connector (waSCC) Host Runtime" 9 | license = "Apache-2.0" 10 | documentation = "https://docs.rs/wascc-host" 11 | readme = "README.md" 12 | keywords = ["webassembly", "wasm", "wasi", "wascc", "wapc"] 13 | categories = ["wasm", "api-bindings","command-line-utilities"] 14 | exclude = ["examples/.assets"] 15 | autotests = false 16 | 17 | [[test]] 18 | name = "integration" 19 | path = "tests/lib.rs" 20 | 21 | [package.metadata.docs.rs] 22 | features = [ "manifest", "lattice" ] 23 | 24 | [badges] 25 | maintenance = { status = "actively-developed" } 26 | 27 | [dependencies] 28 | libloading = "0.6.3" 29 | crossbeam-channel = "0.4.3" 30 | crossbeam = "0.7.3" 31 | crossbeam-utils = "^0.7.0" 32 | prometheus = { version = "0.9", features = ["push"], optional = true } 33 | hyper = { version = "0.13", optional = true } 34 | tokio = { version = "0.2", features = ["macros"] } 35 | wapc = { version = "0.10.0" } 36 | wascc-codec = "0.8" 37 | wascap = "0.5.1" 38 | log = "0.4.11" 39 | rand = "0.7.3" 40 | env_logger = "0.7.1" 41 | ring = "0.16.15" 42 | data-encoding = "2.3.0" 43 | oci-distribution = "0.4.0" 44 | uuid = { version = "0.8", features = ["serde", "v4"] } 45 | futures = "0.3.6" 46 | provider-archive = "0.1.0" 47 | 48 | 49 | # Opt-in dependencies chosen by feature flags 50 | nats = { version = "0.8.1", optional = true } 51 | serde = { version = "1.0", features = ["derive"], optional = true } 52 | serde_yaml = { version = "0.8.13", optional = true } 53 | serde_json = { version = "1.0.57", optional = true } 54 | envmnt = { version = "0.8.4", optional = true } 55 | structopt = { version = "0.3.17", optional = true } 56 | latticeclient = { version = "0.4.0", optional = true } 57 | ctrlc = { version = "3.1.6", features = ["termination"], optional = true} 58 | wasm3-provider = { version = "0.0.1", optional = true} 59 | wasmtime-provider = { version = "0.0.1" , optional = true} 60 | 61 | [dev-dependencies] 62 | reqwest = { version = "0.10", features = ["blocking"] } 63 | mockito = "0.27" 64 | redis = "0.17.0" 65 | nats = "0.8.1" 66 | serde_json = "1.0.57" 67 | 68 | 69 | [features] 70 | default = ["wasmtime"] 71 | manifest = ["serde", "serde_yaml", "serde_json", "envmnt"] 72 | bin = ["structopt", "ctrlc"] 73 | prometheus_middleware = ["prometheus", "hyper"] 74 | lattice = ["nats", "serde", "latticeclient", "serde_json"] 75 | wasmtime = ["wasmtime-provider"] 76 | wasm3 = ["wasm3-provider"] 77 | 78 | [[example]] 79 | name = "kvcounter_manifest" 80 | required-features = ["manifest"] 81 | 82 | [[bin]] 83 | name = "wascc-host" 84 | path = "src/bin.rs" 85 | required-features = ["manifest", "bin"] 86 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | This repository is being superceded by the [wascc-host](https://github.com/wascc/wasccd/tree/main/crates/wascc-host) crate directory in the [wasccd](https://github.com/wascc/wasccd) repository. We expect this change to become official when wascc-host 0.15.0 is released at the end of November 2020. 4 | 5 | Until then, you can continue to use wascc-host 0.14.0. 6 | -------------------------------------------------------------------------------- /examples/.assets/act.nk: -------------------------------------------------------------------------------- 1 | SAAGJYAKODWV42YVRMUAIQFT6KFYT4R6A2LAS3WCH3BJ3KSBFKQOAU5LEU 2 | 3 | -------------------------------------------------------------------------------- /examples/.assets/as-actor.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wascc-host/529f1cfc54341c5acfc614c99ae731323c97176a/examples/.assets/as-actor.wasm -------------------------------------------------------------------------------- /examples/.assets/echo.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wascc-host/529f1cfc54341c5acfc614c99ae731323c97176a/examples/.assets/echo.wasm -------------------------------------------------------------------------------- /examples/.assets/echo2.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wascc-host/529f1cfc54341c5acfc614c99ae731323c97176a/examples/.assets/echo2.wasm -------------------------------------------------------------------------------- /examples/.assets/extras.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wascc-host/529f1cfc54341c5acfc614c99ae731323c97176a/examples/.assets/extras.wasm -------------------------------------------------------------------------------- /examples/.assets/kvcounter.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wascc-host/529f1cfc54341c5acfc614c99ae731323c97176a/examples/.assets/kvcounter.wasm -------------------------------------------------------------------------------- /examples/.assets/kvcounter_tweaked.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wascc-host/529f1cfc54341c5acfc614c99ae731323c97176a/examples/.assets/kvcounter_tweaked.wasm -------------------------------------------------------------------------------- /examples/.assets/libkeyvalue.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wascc-host/529f1cfc54341c5acfc614c99ae731323c97176a/examples/.assets/libkeyvalue.so -------------------------------------------------------------------------------- /examples/.assets/libtest_streams_provider.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wascc-host/529f1cfc54341c5acfc614c99ae731323c97176a/examples/.assets/libtest_streams_provider.so -------------------------------------------------------------------------------- /examples/.assets/libwascc_fs.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wascc-host/529f1cfc54341c5acfc614c99ae731323c97176a/examples/.assets/libwascc_fs.so -------------------------------------------------------------------------------- /examples/.assets/libwascc_httpsrv.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wascc-host/529f1cfc54341c5acfc614c99ae731323c97176a/examples/.assets/libwascc_httpsrv.so -------------------------------------------------------------------------------- /examples/.assets/libwascc_logging.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wascc-host/529f1cfc54341c5acfc614c99ae731323c97176a/examples/.assets/libwascc_logging.so -------------------------------------------------------------------------------- /examples/.assets/libwascc_nats.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wascc-host/529f1cfc54341c5acfc614c99ae731323c97176a/examples/.assets/libwascc_nats.so -------------------------------------------------------------------------------- /examples/.assets/libwascc_redis.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wascc-host/529f1cfc54341c5acfc614c99ae731323c97176a/examples/.assets/libwascc_redis.so -------------------------------------------------------------------------------- /examples/.assets/libwascc_streams_redis.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wascc-host/529f1cfc54341c5acfc614c99ae731323c97176a/examples/.assets/libwascc_streams_redis.so -------------------------------------------------------------------------------- /examples/.assets/logger.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wascc-host/529f1cfc54341c5acfc614c99ae731323c97176a/examples/.assets/logger.wasm -------------------------------------------------------------------------------- /examples/.assets/mod1.nk: -------------------------------------------------------------------------------- 1 | SMAFYSVZ5E6UVTZT4JJPWAGMVCXS7ISC3TYCMGHYHSR6TOM3EO4EB7GFJY 2 | 3 | -------------------------------------------------------------------------------- /examples/.assets/mod2.nk: -------------------------------------------------------------------------------- 1 | SMADOHPTXTC6PYFYQAHEZ4YSASQFX7PWKZPCBYWKEVIE5S7ASPMVKHME6E 2 | 3 | -------------------------------------------------------------------------------- /examples/.assets/mod_extras.nk: -------------------------------------------------------------------------------- 1 | SMAO7V5K6DMONWCIEHYETJXUCQOJLTCFD6NMLVMWWKDEB4KKG4KFC3GJL4 2 | 3 | -------------------------------------------------------------------------------- /examples/.assets/mod_kvcounter.nk: -------------------------------------------------------------------------------- 1 | SMAETBR36TBSCIZH35AHDBRAUCPVOKA5JJNNYKEI5ZK2LETD4JB53V57HM 2 | 3 | -------------------------------------------------------------------------------- /examples/.assets/mod_sub.nk: -------------------------------------------------------------------------------- 1 | SMAK7QVP7ZV4WUPJY5DFU4WCS7KXQEWXHLX765HGVP5GL6IYEUEWVLHSRA 2 | 3 | -------------------------------------------------------------------------------- /examples/.assets/mod_sub2.nk: -------------------------------------------------------------------------------- 1 | SMALT45JHR6G5V4FP6R3MKRTXM66UKRWSTK6S5QNF3N7QTT7CN7FK43RGQ 2 | 3 | -------------------------------------------------------------------------------- /examples/.assets/multibinding.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wascc-host/529f1cfc54341c5acfc614c99ae731323c97176a/examples/.assets/multibinding.wasm -------------------------------------------------------------------------------- /examples/.assets/subscriber.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wascc-host/529f1cfc54341c5acfc614c99ae731323c97176a/examples/.assets/subscriber.wasm -------------------------------------------------------------------------------- /examples/.assets/subscriber2.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wascc-host/529f1cfc54341c5acfc614c99ae731323c97176a/examples/.assets/subscriber2.wasm -------------------------------------------------------------------------------- /examples/.assets/wasi_consumer.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wascc-host/529f1cfc54341c5acfc614c99ae731323c97176a/examples/.assets/wasi_consumer.wasm -------------------------------------------------------------------------------- /examples/.assets/wasi_provider.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wascc-host/529f1cfc54341c5acfc614c99ae731323c97176a/examples/.assets/wasi_provider.wasm -------------------------------------------------------------------------------- /examples/add_remove.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use wascc_host::{Actor, Host, NativeCapability}; 3 | 4 | fn main() -> std::result::Result<(), Box> { 5 | env_logger::init(); 6 | let host = Host::new(); 7 | host.add_actor(Actor::from_file("./examples/.assets/echo.wasm")?)?; 8 | host.add_actor(Actor::from_file("./examples/.assets/echo2.wasm")?)?; 9 | host.add_native_capability(NativeCapability::from_file( 10 | "./examples/.assets/libwascc_httpsrv.so", 11 | None, 12 | )?)?; 13 | 14 | host.set_binding( 15 | "MB4OLDIC3TCZ4Q4TGGOVAZC43VXFE2JQVRAXQMQFXUCREOOFEKOKZTY2", 16 | "wascc:http_server", 17 | None, 18 | generate_port_config(8081), 19 | )?; 20 | host.set_binding( 21 | "MDFD7XZ5KBOPLPHQKHJEMPR54XIW6RAG5D7NNKN22NP7NSEWNTJZP7JN", 22 | "wascc:http_server", 23 | None, 24 | generate_port_config(8082), 25 | )?; 26 | 27 | println!("Actors (before removal):"); 28 | for (id, _claims) in host.actors() { 29 | println!(" - {}", id); 30 | } 31 | println!("Capabilities (before removal)"); 32 | for ((binding_name, capid), _descriptor) in host.capabilities() { 33 | println!("- {},{}", binding_name, capid); 34 | } 35 | 36 | // Need to wait until the HTTP server finishes starting before we 37 | // should try and kill it. 38 | println!("Sleeping 1s..."); 39 | std::thread::sleep(std::time::Duration::from_millis(1000)); 40 | 41 | println!("Removing echo actor 2..."); 42 | // This will terminate the actor and free up the HTTP port 43 | host.remove_actor("MB4OLDIC3TCZ4Q4TGGOVAZC43VXFE2JQVRAXQMQFXUCREOOFEKOKZTY2")?; 44 | 45 | println!("Sleeping 2s..."); 46 | std::thread::sleep(std::time::Duration::from_millis(1000)); 47 | 48 | println!("Actors (after removal of second echo):"); 49 | for (id, _claims) in host.actors() { 50 | println!(" - {}", id); 51 | } 52 | 53 | // .. 54 | // at this point, curling on port 8081 should fail w/connection refused 55 | // while curling on port 8082 should work just fine 56 | 57 | std::thread::park(); 58 | 59 | Ok(()) 60 | } 61 | 62 | fn generate_port_config(port: u16) -> HashMap { 63 | let mut hm = HashMap::new(); 64 | hm.insert("PORT".to_string(), port.to_string()); 65 | 66 | hm 67 | } 68 | -------------------------------------------------------------------------------- /examples/assemblyscript.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use wascc_host::{Actor, Host, NativeCapability}; 3 | 4 | fn main() -> std::result::Result<(), Box> { 5 | env_logger::init(); 6 | let host = Host::new(); 7 | host.add_actor(Actor::from_file("./examples/.assets/as-actor.wasm")?)?; 8 | host.add_native_capability(NativeCapability::from_file( 9 | "./examples/.assets/libwascc_httpsrv.so", 10 | None, 11 | )?)?; 12 | host.add_native_capability(NativeCapability::from_file( 13 | "./examples/.assets/libwascc_redis.so", 14 | None, 15 | )?)?; 16 | 17 | host.set_binding( 18 | "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ", 19 | "wascc:http_server", 20 | None, 21 | generate_port_config(8081), 22 | )?; 23 | 24 | host.set_binding( 25 | "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ", 26 | "wascc:keyvalue", 27 | None, 28 | redis_config(), 29 | )?; 30 | 31 | std::thread::park(); 32 | 33 | Ok(()) 34 | } 35 | 36 | fn generate_port_config(port: u16) -> HashMap { 37 | let mut hm = HashMap::new(); 38 | hm.insert("PORT".to_string(), port.to_string()); 39 | 40 | hm 41 | } 42 | 43 | fn redis_config() -> HashMap { 44 | let mut hm = HashMap::new(); 45 | hm.insert("URL".to_string(), "redis://127.0.0.1:6379".to_string()); 46 | 47 | hm 48 | } 49 | -------------------------------------------------------------------------------- /examples/echo_middleware.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use wascc_host::middleware::{InvocationHandler, MiddlewareResponse}; 3 | use wascc_host::{Actor, Host, Invocation, InvocationResponse, Middleware, NativeCapability}; 4 | 5 | #[macro_use] 6 | extern crate log; 7 | 8 | type Result = std::result::Result; 9 | 10 | fn main() -> std::result::Result<(), Box> { 11 | env_logger::init(); 12 | let host = Host::new(); 13 | host.add_actor(Actor::from_file("./examples/.assets/echo.wasm")?)?; 14 | host.add_actor(Actor::from_file("./examples/.assets/echo2.wasm")?)?; 15 | host.add_native_capability(NativeCapability::from_file( 16 | "./examples/.assets/libwascc_httpsrv.so", 17 | None, 18 | )?)?; 19 | 20 | host.add_middleware(CachingMiddleware::default()); 21 | host.add_middleware(LoggingMiddleware::default()); 22 | 23 | host.set_binding( 24 | "MB4OLDIC3TCZ4Q4TGGOVAZC43VXFE2JQVRAXQMQFXUCREOOFEKOKZTY2", 25 | "wascc:http_server", 26 | None, 27 | generate_port_config(8081), 28 | )?; 29 | host.set_binding( 30 | "MDFD7XZ5KBOPLPHQKHJEMPR54XIW6RAG5D7NNKN22NP7NSEWNTJZP7JN", 31 | "wascc:http_server", 32 | None, 33 | generate_port_config(8082), 34 | )?; 35 | 36 | std::thread::park(); 37 | 38 | Ok(()) 39 | } 40 | 41 | fn generate_port_config(port: u16) -> HashMap { 42 | let mut hm = HashMap::new(); 43 | hm.insert("PORT".to_string(), port.to_string()); 44 | 45 | hm 46 | } 47 | 48 | #[derive(Default)] 49 | struct CachingMiddleware {} 50 | 51 | impl Middleware for CachingMiddleware { 52 | fn actor_pre_invoke(&self, inv: Invocation) -> wascc_host::Result { 53 | info!("CachingMiddleware-ACTOR(PRE): {}", inv.operation); 54 | Ok(inv) 55 | } 56 | 57 | fn actor_invoke( 58 | &self, 59 | inv: Invocation, 60 | _handler: InvocationHandler, 61 | ) -> Result { 62 | info!("CachingMiddleware-ACTOR(INV): cached response returned, execution halted"); 63 | Ok(MiddlewareResponse::Halt(InvocationResponse::success( 64 | &inv, 65 | "cached actor response".as_bytes().to_vec(), 66 | ))) 67 | } 68 | 69 | fn actor_post_invoke( 70 | &self, 71 | response: InvocationResponse, 72 | ) -> wascc_host::Result { 73 | info!( 74 | "CachingMiddleware-ACTOR(POST): success: {}", 75 | response.error.is_none() 76 | ); 77 | Ok(response) 78 | } 79 | 80 | fn capability_pre_invoke(&self, inv: Invocation) -> wascc_host::Result { 81 | info!("CachingMiddleware-CAP(PRE): {}", inv.operation); 82 | Ok(inv) 83 | } 84 | 85 | fn capability_invoke( 86 | &self, 87 | inv: Invocation, 88 | _handler: InvocationHandler, 89 | ) -> Result { 90 | info!("CachingMiddleware-CAP(INV): cached response returned, execution halted"); 91 | Ok(MiddlewareResponse::Halt(InvocationResponse::success( 92 | &inv, 93 | "cached capability response".as_bytes().to_vec(), 94 | ))) 95 | } 96 | 97 | fn capability_post_invoke( 98 | &self, 99 | response: InvocationResponse, 100 | ) -> wascc_host::Result { 101 | info!( 102 | "CachingMiddleware-CAP(POST): success: {}", 103 | response.error.is_none() 104 | ); 105 | Ok(response) 106 | } 107 | } 108 | 109 | #[derive(Default)] 110 | struct LoggingMiddleware {} 111 | 112 | impl Middleware for LoggingMiddleware { 113 | fn actor_pre_invoke(&self, inv: Invocation) -> wascc_host::Result { 114 | info!("LoggingMiddleware-ACTOR(PRE): {}", inv.operation); 115 | Ok(inv) 116 | } 117 | 118 | fn actor_invoke( 119 | &self, 120 | inv: Invocation, 121 | handler: InvocationHandler, 122 | ) -> Result { 123 | // Will not be invoked since the 'CachingMiddleware' halts the middleware execution 124 | info!("LoggingMiddleware-ACTOR(INV): {}", inv.operation); 125 | Ok(MiddlewareResponse::Continue(handler.invoke(inv))) 126 | } 127 | 128 | fn actor_post_invoke( 129 | &self, 130 | response: InvocationResponse, 131 | ) -> wascc_host::Result { 132 | info!( 133 | "LoggingMiddleware-ACTOR(POST): success: {}", 134 | response.error.is_none() 135 | ); 136 | Ok(response) 137 | } 138 | 139 | fn capability_pre_invoke(&self, inv: Invocation) -> wascc_host::Result { 140 | info!("LoggingMiddleware-CAP(PRE): {}", inv.operation); 141 | Ok(inv) 142 | } 143 | 144 | fn capability_invoke( 145 | &self, 146 | inv: Invocation, 147 | handler: InvocationHandler, 148 | ) -> Result { 149 | // Will not be invoked since the 'CachingMiddleware' halts the middleware execution 150 | info!("LoggingMiddleware-CAP(INV): {}", inv.operation); 151 | Ok(MiddlewareResponse::Continue(handler.invoke(inv))) 152 | } 153 | 154 | fn capability_post_invoke( 155 | &self, 156 | response: InvocationResponse, 157 | ) -> wascc_host::Result { 158 | info!( 159 | "LoggingMiddleware-CAP(POST): success: {}", 160 | response.error.is_none() 161 | ); 162 | Ok(response) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /examples/echoserver.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use wascc_host::{Actor, Host, NativeCapability}; 3 | 4 | fn main() -> std::result::Result<(), Box> { 5 | env_logger::init(); 6 | let host = Host::new(); 7 | host.add_actor(Actor::from_file("./examples/.assets/echo.wasm")?)?; 8 | host.add_actor(Actor::from_file("./examples/.assets/echo2.wasm")?)?; 9 | host.add_native_capability(NativeCapability::from_file( 10 | "./examples/.assets/libwascc_httpsrv.so", 11 | None, 12 | )?)?; 13 | 14 | host.set_binding( 15 | "MB4OLDIC3TCZ4Q4TGGOVAZC43VXFE2JQVRAXQMQFXUCREOOFEKOKZTY2", 16 | "wascc:http_server", 17 | None, 18 | generate_port_config(8081), 19 | )?; 20 | host.set_binding( 21 | "MDFD7XZ5KBOPLPHQKHJEMPR54XIW6RAG5D7NNKN22NP7NSEWNTJZP7JN", 22 | "wascc:http_server", 23 | None, 24 | generate_port_config(8082), 25 | )?; 26 | 27 | std::thread::park(); 28 | 29 | Ok(()) 30 | } 31 | 32 | fn generate_port_config(port: u16) -> HashMap { 33 | let mut hm = HashMap::new(); 34 | hm.insert("PORT".to_string(), port.to_string()); 35 | 36 | hm 37 | } 38 | -------------------------------------------------------------------------------- /examples/extras.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use wascc_host::{Actor, Host, NativeCapability}; 3 | 4 | fn main() -> std::result::Result<(), Box> { 5 | env_logger::init(); 6 | let host = Host::new(); 7 | host.add_actor(Actor::from_file("./examples/.assets/extras.wasm")?)?; 8 | host.add_native_capability(NativeCapability::from_file( 9 | "./examples/.assets/libwascc_httpsrv.so", 10 | None, 11 | )?)?; 12 | 13 | host.set_binding( 14 | "MDOYAT2KHJ6N5DAY5X7JKGIBMKABTPXRX2KHUJI6APOVNKQDMRTIUSY2", 15 | "wascc:http_server", 16 | None, 17 | http_config(), 18 | )?; 19 | 20 | std::thread::park(); 21 | 22 | Ok(()) 23 | } 24 | 25 | fn http_config() -> HashMap { 26 | let mut hm = HashMap::new(); 27 | hm.insert("PORT".to_string(), "8081".to_string()); 28 | 29 | hm 30 | } 31 | -------------------------------------------------------------------------------- /examples/kvcounter.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use wascc_host::{Actor, Host, NativeCapability}; 3 | 4 | fn main() -> std::result::Result<(), Box> { 5 | env_logger::init(); 6 | let host = Host::new(); 7 | host.add_actor(Actor::from_file("./examples/.assets/kvcounter.wasm")?)?; 8 | host.add_native_capability(NativeCapability::from_file( 9 | "./examples/.assets/libwascc_httpsrv.so", 10 | None, 11 | )?)?; 12 | host.add_native_capability(NativeCapability::from_file( 13 | "./examples/.assets/libwascc_redis.so", 14 | None, 15 | )?)?; 16 | 17 | host.set_binding( 18 | "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ", 19 | "wascc:keyvalue", 20 | None, 21 | redis_config(), 22 | )?; 23 | host.set_binding( 24 | "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ", 25 | "wascc:http_server", 26 | None, 27 | http_config(), 28 | )?; 29 | 30 | std::thread::park(); 31 | 32 | Ok(()) 33 | } 34 | 35 | fn redis_config() -> HashMap { 36 | let mut hm = HashMap::new(); 37 | hm.insert("URL".to_string(), "redis://127.0.0.1:6379".to_string()); 38 | 39 | hm 40 | } 41 | 42 | fn http_config() -> HashMap { 43 | let mut hm = HashMap::new(); 44 | hm.insert("PORT".to_string(), "8081".to_string()); 45 | 46 | hm 47 | } 48 | -------------------------------------------------------------------------------- /examples/kvcounter_manifest.rs: -------------------------------------------------------------------------------- 1 | // An example demonstrating loading a manifest 2 | // For this to run, you'll need the `manifest` feature enabled. You can do that as follows from the 3 | // root wascc-host directory: 4 | // cargo run --example kvcounter_manfifest --features "manifest" 5 | 6 | use wascc_host::{Host, HostManifest}; 7 | 8 | fn main() -> std::result::Result<(), Box> { 9 | let _ = env_logger::Builder::from_env( 10 | env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "wascc_host=info"), 11 | ) 12 | .format_module_path(false) 13 | .try_init(); 14 | let host = Host::new(); 15 | host.apply_manifest(HostManifest::from_path( 16 | "./examples/sample_manifest.yaml", 17 | true, 18 | )?)?; 19 | 20 | std::thread::park(); 21 | Ok(()) 22 | } 23 | -------------------------------------------------------------------------------- /examples/lattice/host1.yaml: -------------------------------------------------------------------------------- 1 | # This host is to contain nothing but the echo server actor. 2 | # NOTE that the HTTP server provider is NOT included in this host. 3 | # Ensure that you launch host2.yaml and host3.yaml before launching this one. 4 | --- 5 | actors: 6 | - ./examples/.assets/echo.wasm 7 | capabilities: [] 8 | bindings: 9 | - actor: "MB4OLDIC3TCZ4Q4TGGOVAZC43VXFE2JQVRAXQMQFXUCREOOFEKOKZTY2" 10 | capability: "wascc:http_server" 11 | values: 12 | PORT: "8081" 13 | - actor: "MB4OLDIC3TCZ4Q4TGGOVAZC43VXFE2JQVRAXQMQFXUCREOOFEKOKZTY2" 14 | capability: "wascc:http_server" 15 | values: 16 | PORT: "8082" 17 | -------------------------------------------------------------------------------- /examples/lattice/host2.yaml: -------------------------------------------------------------------------------- 1 | # Loads a host that starts off with nothing but an unbound HTTP server provider 2 | # NOTE that there are no actors loaded 3 | --- 4 | actors: [] 5 | capabilities: 6 | - path: ./examples/.assets/libwascc_httpsrv.so 7 | bindings: [] 8 | -------------------------------------------------------------------------------- /examples/lattice/host3.yaml: -------------------------------------------------------------------------------- 1 | # Loads a host that starts off with nothing but an unbound HTTP server provider 2 | # NOTE that there are no actors loaded 3 | --- 4 | actors: [] 5 | capabilities: 6 | - path: ./examples/.assets/libwascc_httpsrv.so 7 | bindings: [] 8 | -------------------------------------------------------------------------------- /examples/logger.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use wascc_host::{Actor, Host, NativeCapability}; 3 | 4 | fn main() -> std::result::Result<(), Box> { 5 | env_logger::init(); 6 | let host = Host::new(); 7 | host.add_actor(Actor::from_file("./examples/.assets/logger.wasm")?)?; 8 | host.add_native_capability(NativeCapability::from_file( 9 | "./examples/.assets/libwascc_httpsrv.so", 10 | None, 11 | )?)?; 12 | host.add_native_capability(NativeCapability::from_file( 13 | "./examples/.assets/libwascc_logging.so", 14 | None, 15 | )?)?; 16 | 17 | host.set_binding( 18 | "MDW7BWQDVYBRC6WKSJRRZL27R73EVBWQINYLPFDRCWDZDFQO4JMO4U6J", 19 | "wascc:http_server", 20 | None, 21 | generate_port_config(8081), 22 | )?; 23 | 24 | // As of waSCC 0.9.0, an actor cannot communicate with a capability 25 | // provider _at all_ unless an explicit bind takes place, even if there is 26 | // no configuration data. This is because bindings in 0.9.0 can be global 27 | // entities, spanning clouds, data centers, and devices. 28 | host.set_binding( 29 | "MDW7BWQDVYBRC6WKSJRRZL27R73EVBWQINYLPFDRCWDZDFQO4JMO4U6J", 30 | "wascc:logging", 31 | None, 32 | HashMap::new(), 33 | )?; 34 | 35 | std::thread::park(); 36 | 37 | Ok(()) 38 | } 39 | 40 | fn generate_port_config(port: u16) -> HashMap { 41 | let mut hm = HashMap::new(); 42 | hm.insert("PORT".to_string(), port.to_string()); 43 | 44 | hm 45 | } 46 | -------------------------------------------------------------------------------- /examples/named_bindings.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * The named bindings example takes an actor that performs an atomic increrement 3 | * on two different data stores and returns the combined result of the counts 4 | * in a single HTTP response. This illustrates the ability to create an actor 5 | * that has multiple, distinguishable capabilities of the same type without 6 | * ever creating a tight coupling. 7 | */ 8 | use std::collections::HashMap; 9 | use wascc_host::{Actor, Host, NativeCapability}; 10 | 11 | const ACTOR_SUBJECT: &str = "MCYWHMJW5VW5U7ZRDQV7JU45GHSR2SA6OZJ2MPHQCFALR2CGFA2NAMZM"; 12 | fn main() -> std::result::Result<(), Box> { 13 | env_logger::init(); 14 | let host = Host::new(); 15 | host.add_actor(Actor::from_file("./examples/.assets/multibinding.wasm")?)?; 16 | host.add_native_capability(NativeCapability::from_file( 17 | "./examples/.assets/libwascc_httpsrv.so", 18 | None, 19 | )?)?; 20 | 21 | // Add TWO Redis providers. Note that these do NOT have to be the same provider... 22 | // we could use Redis and an in-memory store or Redis and Cassandra or 23 | // Cassandra and etcd and so on 24 | host.add_native_capability(NativeCapability::from_file( 25 | "./examples/.assets/libwascc_redis.so", 26 | Some("source1".to_string()), 27 | )?)?; 28 | host.add_native_capability(NativeCapability::from_file( 29 | "./examples/.assets/libwascc_redis.so", 30 | Some("source2".to_string()), 31 | )?)?; 32 | 33 | host.set_binding( 34 | ACTOR_SUBJECT, 35 | "wascc:http_server", 36 | None, 37 | generate_port_config(8081), 38 | )?; 39 | 40 | host.set_binding( 41 | ACTOR_SUBJECT, 42 | "wascc:keyvalue", 43 | Some("source1".to_string()), 44 | redis_config(0), 45 | )?; 46 | 47 | host.set_binding( 48 | ACTOR_SUBJECT, 49 | "wascc:keyvalue", 50 | Some("source2".to_string()), 51 | redis_config(1), 52 | )?; 53 | 54 | std::thread::park(); 55 | 56 | Ok(()) 57 | } 58 | 59 | fn redis_config(dbindex: u32) -> HashMap { 60 | let mut hm = HashMap::new(); 61 | hm.insert( 62 | "URL".to_string(), 63 | format!("redis://127.0.0.1:6379/{}", dbindex), 64 | ); 65 | 66 | hm 67 | } 68 | 69 | fn generate_port_config(port: u16) -> HashMap { 70 | let mut hm = HashMap::new(); 71 | hm.insert("PORT".to_string(), port.to_string()); 72 | 73 | hm 74 | } 75 | -------------------------------------------------------------------------------- /examples/oci_test.yaml: -------------------------------------------------------------------------------- 1 | # This file demonstrates how to use OCI references in a manifest. This feature is 2 | # not yet fully stable, and requires that the images referenced below be pushed to 3 | # the given registry 4 | --- 5 | labels: 6 | sample: "Key-Value Counter" 7 | actors: 8 | - "wascc.azurecr.io/keyvalue:v1" 9 | capabilities: 10 | - path: wascc.azurecr.io/redis-provider:v1 11 | - path: wascc.azurecr.io/httpsrv-provider:v1 12 | bindings: 13 | - actor: "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ" 14 | capability: "wascc:keyvalue" 15 | values: 16 | URL: redis://127.0.0.1:6379 17 | - actor: "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ" 18 | capability: "wascc:http_server" 19 | values: 20 | PORT: "8081" 21 | -------------------------------------------------------------------------------- /examples/portable_provider.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use wascc_host::{Actor, Host, NativeCapability, WasiParams}; 3 | 4 | fn main() -> std::result::Result<(), Box> { 5 | env_logger::init(); 6 | let host = Host::new(); 7 | host.add_actor(Actor::from_file("./examples/.assets/wasi_consumer.wasm")?)?; 8 | host.add_capability( 9 | Actor::from_file("./examples/.assets/wasi_provider.wasm")?, 10 | None, 11 | WasiParams::default(), 12 | )?; // WASI default does not map file system access 13 | 14 | host.add_native_capability(NativeCapability::from_file( 15 | "./examples/.assets/libwascc_httpsrv.so", 16 | None, 17 | )?)?; 18 | 19 | host.set_binding( 20 | "MDNPIQOU5EEHTP4TKY2APFOJTTEYYARN3ZIJTRWRYWHX6B4MFSO6ZCRT", 21 | "wascc:wasidemo", 22 | None, 23 | HashMap::new(), 24 | )?; 25 | 26 | host.set_binding( 27 | "MDNPIQOU5EEHTP4TKY2APFOJTTEYYARN3ZIJTRWRYWHX6B4MFSO6ZCRT", 28 | "wascc:http_server", 29 | None, 30 | generate_port_config(8081), 31 | )?; 32 | 33 | for (_rk, descriptor) in host.capabilities() { 34 | println!(" ** Capability providers in Host:\n"); 35 | println!( 36 | "\t'{}' v{} ({}) for {}", 37 | descriptor.name, descriptor.version, descriptor.revision, descriptor.id 38 | ); 39 | } 40 | for (actor, _claims) in host.actors() { 41 | println!(" ** Actors in Host:\n"); 42 | println!("\t{}", actor); 43 | } 44 | 45 | std::thread::park(); 46 | 47 | Ok(()) 48 | } 49 | 50 | fn generate_port_config(port: u16) -> HashMap { 51 | let mut hm = HashMap::new(); 52 | hm.insert("PORT".to_string(), port.to_string()); 53 | 54 | hm 55 | } 56 | -------------------------------------------------------------------------------- /examples/prometheus_middleware.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "prometheus_middleware")] 2 | fn main() -> std::result::Result<(), Box> { 3 | prometheus_example::run_example() 4 | } 5 | 6 | #[cfg(not(feature = "prometheus_middleware"))] 7 | fn main() -> std::result::Result<(), Box> { 8 | println!("Feature 'prometheus_middleware' needed to run this example"); 9 | Ok(()) 10 | } 11 | 12 | #[cfg(feature = "prometheus_middleware")] 13 | mod prometheus_example { 14 | use std::collections::HashMap; 15 | use std::net::SocketAddr; 16 | use wascc_host::middleware::prometheus::{PrometheusConfig, PrometheusMiddleware}; 17 | use wascc_host::{Actor, Host, NativeCapability}; 18 | 19 | pub fn run_example() -> std::result::Result<(), Box> { 20 | env_logger::init(); 21 | let host = Host::new(); 22 | host.add_actor(Actor::from_file("./examples/.assets/echo.wasm")?)?; 23 | host.add_actor(Actor::from_file("./examples/.assets/echo2.wasm")?)?; 24 | host.add_native_capability(NativeCapability::from_file( 25 | "./examples/.assets/libwascc_httpsrv.so", 26 | None, 27 | )?)?; 28 | 29 | let server_addr: SocketAddr = ([127, 0, 0, 1], 9898).into(); 30 | let config = PrometheusConfig { 31 | metrics_server_addr: Some(server_addr), 32 | pushgateway_config: None, 33 | moving_average_window_size: None, 34 | }; 35 | host.add_middleware(PrometheusMiddleware::new(config).unwrap()); 36 | 37 | host.set_binding( 38 | "MB4OLDIC3TCZ4Q4TGGOVAZC43VXFE2JQVRAXQMQFXUCREOOFEKOKZTY2", 39 | "wascc:http_server", 40 | None, 41 | generate_port_config(8081), 42 | )?; 43 | host.set_binding( 44 | "MDFD7XZ5KBOPLPHQKHJEMPR54XIW6RAG5D7NNKN22NP7NSEWNTJZP7JN", 45 | "wascc:http_server", 46 | None, 47 | generate_port_config(8082), 48 | )?; 49 | 50 | std::thread::park(); 51 | 52 | Ok(()) 53 | } 54 | 55 | fn generate_port_config(port: u16) -> HashMap { 56 | let mut hm = HashMap::new(); 57 | hm.insert("PORT".to_string(), port.to_string()); 58 | 59 | hm 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/replace.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::io; 3 | use wascc_host::{Actor, Host, NativeCapability}; 4 | 5 | fn main() -> std::result::Result<(), Box> { 6 | env_logger::init(); 7 | let host = Host::new(); 8 | host.add_actor(Actor::from_file("./examples/.assets/kvcounter.wasm")?)?; 9 | host.add_native_capability(NativeCapability::from_file( 10 | "./examples/.assets/libwascc_httpsrv.so", 11 | None, 12 | )?)?; 13 | host.add_native_capability(NativeCapability::from_file( 14 | "./examples/.assets/libwascc_redis.so", 15 | None, 16 | )?)?; 17 | 18 | host.set_binding( 19 | "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ", 20 | "wascc:keyvalue", 21 | None, 22 | redis_config(), 23 | )?; 24 | host.set_binding( 25 | "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ", 26 | "wascc:http_server", 27 | None, 28 | http_config(), 29 | )?; 30 | 31 | println!( 32 | "**> curl localhost:8081/counter1 to test, then press ENTER to replace with a new version" 33 | ); 34 | let mut input = String::new(); 35 | io::stdin().read_line(&mut input)?; 36 | // Note that we don't supply the public key of the one to replace, it 37 | // comes straight from the Actor being replaced. You cannot replace actors 38 | // that do not have the same public key as a security measure against 39 | // malicious code 40 | host.replace_actor(Actor::from_file( 41 | "./examples/.assets/kvcounter_tweaked.wasm", 42 | )?)?; 43 | println!("**> KV counter replaced, issue query to see the new module running."); 44 | 45 | println!("**> Press ENTER to remove the key-value provider"); 46 | io::stdin().read_line(&mut input)?; 47 | host.remove_native_capability("wascc:keyvalue", None)?; 48 | 49 | println!("**> Press ENTER to add an in-memory key-value provider"); 50 | io::stdin().read_line(&mut input)?; 51 | host.add_native_capability(NativeCapability::from_file( 52 | "./examples/.assets/libkeyvalue.so", 53 | None, 54 | )?)?; 55 | 56 | println!("**> Now your counter should have started over."); 57 | 58 | std::thread::park(); 59 | 60 | Ok(()) 61 | } 62 | 63 | fn redis_config() -> HashMap { 64 | let mut hm = HashMap::new(); 65 | hm.insert("URL".to_string(), "redis://127.0.0.1:6379".to_string()); 66 | 67 | hm 68 | } 69 | 70 | fn http_config() -> HashMap { 71 | let mut hm = HashMap::new(); 72 | hm.insert("PORT".to_string(), "8081".to_string()); 73 | 74 | hm 75 | } 76 | -------------------------------------------------------------------------------- /examples/replace_ui/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | package-lock.json 4 | nginx.conf 5 | 6 | -------------------------------------------------------------------------------- /examples/replace_ui/README.md: -------------------------------------------------------------------------------- 1 | # Replace GUI 2 | 3 | This is a UI designed to be used while running the replace example. To start the replace example back-end, run the following from the `wascc-host` root folder: 4 | 5 | ```shell 6 | RUST_LOG=info,cranelift_wasm=warn cargo run --example replace 7 | ``` 8 | 9 | For this back end, you will need `Redis` installed to demonstrate one of the two key-value store capability providers. 10 | 11 | Then, start the UI: 12 | 13 | ```shell 14 | ./start.sh 15 | ``` 16 | 17 | Note that you will need `nginx` installed and you might need to `sudo` the start script if you run into permissions problems writing to the nginx log directory. 18 | -------------------------------------------------------------------------------- /examples/replace_ui/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wascc-host/529f1cfc54341c5acfc614c99ae731323c97176a/examples/replace_ui/favicon.ico -------------------------------------------------------------------------------- /examples/replace_ui/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{ name }}", 3 | "short_name": "{{ name }}", 4 | "start_url": "/", 5 | "display": "standalone", 6 | "orientation": "portrait", 7 | "background_color": "#fff", 8 | "theme_color": "#673ab8", 9 | "icons": [{ 10 | "src": "/assets/icon.png", 11 | "type": "image/png", 12 | "sizes": "512x512" 13 | }] 14 | } 15 | -------------------------------------------------------------------------------- /examples/replace_ui/mime.types: -------------------------------------------------------------------------------- 1 | types { 2 | text/html html htm shtml; 3 | text/css css; 4 | text/xml xml rss; 5 | image/gif gif; 6 | image/jpeg jpeg jpg; 7 | application/x-javascript js; 8 | text/plain txt; 9 | text/x-component htc; 10 | text/mathml mml; 11 | image/png png; 12 | image/x-icon ico; 13 | image/x-jng jng; 14 | image/vnd.wap.wbmp wbmp; 15 | application/java-archive jar war ear; 16 | application/mac-binhex40 hqx; 17 | application/pdf pdf; 18 | application/x-cocoa cco; 19 | application/x-java-archive-diff jardiff; 20 | application/x-java-jnlp-file jnlp; 21 | application/x-makeself run; 22 | application/x-perl pl pm; 23 | application/x-pilot prc pdb; 24 | application/x-rar-compressed rar; 25 | application/x-redhat-package-manager rpm; 26 | application/x-sea sea; 27 | application/x-shockwave-flash swf; 28 | application/x-stuffit sit; 29 | application/x-tcl tcl tk; 30 | application/x-x509-ca-cert der pem crt; 31 | application/x-xpinstall xpi; 32 | application/zip zip; 33 | application/octet-stream deb; 34 | application/octet-stream bin exe dll; 35 | application/octet-stream dmg; 36 | application/octet-stream eot; 37 | application/octet-stream iso img; 38 | application/octet-stream msi msp msm; 39 | audio/mpeg mp3; 40 | audio/x-realaudio ra; 41 | video/mpeg mpeg mpg; 42 | video/quicktime mov; 43 | video/x-flv flv; 44 | video/x-msvideo avi; 45 | video/x-ms-wmv wmv; 46 | video/x-ms-asf asx asf; 47 | video/x-mng mng; 48 | } 49 | 50 | -------------------------------------------------------------------------------- /examples/replace_ui/nginx.template: -------------------------------------------------------------------------------- 1 | daemon off; 2 | 3 | events { 4 | worker_connections 4096; ## Default: 1024 5 | } 6 | 7 | http { 8 | index index.html index.htm; 9 | include mime.types; 10 | 11 | server { 12 | root DIR/build; 13 | 14 | listen 8080; 15 | 16 | location /counter1 { 17 | proxy_pass http://127.0.0.1:8081/; 18 | proxy_http_version 1.1; 19 | proxy_set_header Upgrade $http_upgrade; 20 | proxy_set_header Connection "upgrade"; 21 | proxy_set_header X-Real-IP $remote_addr; 22 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 23 | proxy_set_header X-Forwarded-Proto $scheme; 24 | proxy_set_header X-Frame-Options SAMEORIGIN; 25 | proxy_read_timeout 86400; 26 | } 27 | 28 | location / { 29 | root DIR/build; 30 | } 31 | 32 | location /favicon.ico { 33 | root DIR; 34 | } 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /examples/replace_ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "replace_ui", 4 | "version": "0.0.0", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "if-env NODE_ENV=production && npm run -s serve || npm run -s dev", 8 | "build": "preact build", 9 | "serve": "preact build && preact serve", 10 | "dev": "preact watch", 11 | "lint": "eslint src" 12 | }, 13 | "eslintConfig": { 14 | "extends": "eslint-config-synacor" 15 | }, 16 | "eslintIgnore": [ 17 | "build/*" 18 | ], 19 | "devDependencies": { 20 | "eslint": "^6.0.1", 21 | "eslint-config-synacor": "^3.0.4", 22 | "if-env": "^1.0.0", 23 | "preact-cli": "^3.0.0-rc.6" 24 | }, 25 | "dependencies": { 26 | "preact": "^10.1.0", 27 | "preact-render-to-string": "^5.1.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/replace_ui/src/index.js: -------------------------------------------------------------------------------- 1 | import './style'; 2 | import { Component } from 'preact'; 3 | 4 | const COUNTER_URL = "http://localhost:8080/counter1" 5 | 6 | export default class App extends Component { 7 | constructor(props) { 8 | super(props) 9 | 10 | this.state = { 11 | payload: null, 12 | error: "", 13 | } 14 | } 15 | 16 | render() { 17 | return ( 18 |
19 | 20 |

waSCC Counter Demo

21 |
22 | 23 | {this.state.payload ? 24 |
25 | {JSON.stringify(this.state.payload).replace(",", ",\n ").replace("{", "{\n ").replace("}", "\n}")} 26 |
27 | : 28 |
29 | secret 30 |
31 |
32 | } 33 |
34 |
35 |

No boilerplate!

36 | 37 |
38 | ); 39 | } 40 | 41 | counterRequest = () => { 42 | fetch(COUNTER_URL).then(d => d.json()).then(r => this.setState({ counter: r.counter, tweaked: r.tweaked, payloadRequested: true, payload: r })).catch(e => this.setState({ payload: { "error": "error making request" } })) 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /examples/replace_ui/src/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | font: 36px/1.21 "Helvetica Neue", arial, sans-serif; 4 | font-weight: 400; 5 | background: #fff; 6 | } 7 | 8 | div.main { 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | flex-direction: column; 13 | } 14 | 15 | div.request { 16 | display: flex; 17 | justify-content: flex-start; 18 | align-items: center; 19 | flex-direction: row; 20 | height: 175px; 21 | width: 650px; 22 | } 23 | 24 | div.payload { 25 | font-family: monospace; 26 | white-space: pre-wrap; 27 | } 28 | 29 | h1 { 30 | text-align: center; 31 | } 32 | 33 | a { 34 | position: relative; 35 | display: inline-block; 36 | padding: 1.2em 2em; 37 | text-decoration: none; 38 | text-align: center; 39 | cursor: pointer; 40 | user-select: none; 41 | color: white; 42 | margin-right: 25px; 43 | } 44 | 45 | a::before { 46 | content: ""; 47 | position: absolute; 48 | top: 0; 49 | left: 0; 50 | bottom: 0; 51 | right: 0; 52 | background: linear-gradient(135deg, #6e8efb, #a777e3); 53 | border-radius: 4px; 54 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); 55 | } 56 | 57 | a::after { 58 | position: relative; 59 | display: inline-block; 60 | content: attr(data-title); 61 | transition: transform 0.2s ease; 62 | font-weight: bold; 63 | letter-spacing: 0.01em; 64 | } 65 | 66 | @media only screen and (max-width: 790px) { 67 | div.request { 68 | flex-direction: column; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/replace_ui/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | DIR=$(pwd) 6 | 7 | sed "s,DIR,$DIR," < nginx.template > nginx.conf 8 | 9 | npm install 10 | npm run build # Only necessary if you've lost your static build files. 11 | 12 | cat $DIR/nginx.conf 13 | 14 | nginx -c $DIR/nginx.conf -------------------------------------------------------------------------------- /examples/sample_manifest.yaml: -------------------------------------------------------------------------------- 1 | # This file provides the same runtime host as the kvcounter.rs sample, but in manifest form 2 | # To see this in action, `cargo run --example kvcounter_manifest --all-features` from the 3 | # wascc-host root directory 4 | --- 5 | labels: 6 | sample: "Key-Value Counter" 7 | actors: 8 | - ./examples/.assets/kvcounter.wasm 9 | capabilities: 10 | - path: ./examples/.assets/libwascc_redis.so 11 | - path: ./examples/.assets/libwascc_httpsrv.so 12 | bindings: 13 | - actor: "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ" 14 | capability: "wascc:keyvalue" 15 | values: 16 | URL: redis://127.0.0.1:6379 17 | - actor: "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ" 18 | capability: "wascc:http_server" 19 | values: 20 | PORT: "8081" 21 | -------------------------------------------------------------------------------- /examples/start_stop.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::io; 3 | use wascc_host::{Actor, Host, NativeCapability}; 4 | 5 | fn main() -> std::result::Result<(), Box> { 6 | env_logger::init(); 7 | let host = Host::new(); 8 | host.add_actor(Actor::from_file("./examples/.assets/kvcounter.wasm")?)?; 9 | host.add_actor(Actor::from_file("./examples/.assets/echo.wasm")?)?; // This actor doesn't get removed 10 | host.add_native_capability(NativeCapability::from_file( 11 | "./examples/.assets/libwascc_httpsrv.so", 12 | None, 13 | )?)?; 14 | host.add_native_capability(NativeCapability::from_file( 15 | "./examples/.assets/libwascc_redis.so", 16 | None, 17 | )?)?; 18 | 19 | host.set_binding( 20 | "MB4OLDIC3TCZ4Q4TGGOVAZC43VXFE2JQVRAXQMQFXUCREOOFEKOKZTY2", 21 | "wascc:http_server", 22 | None, 23 | generate_port_config(8082), 24 | )?; 25 | 26 | host.set_binding( 27 | "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ", 28 | "wascc:keyvalue", 29 | None, 30 | redis_config(), 31 | )?; 32 | host.set_binding( 33 | "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ", 34 | "wascc:http_server", 35 | None, 36 | generate_port_config(8081), 37 | )?; 38 | 39 | println!( 40 | "**> curl localhost:8081/counter1 to test, then press ENTER to remove the actor from the host" 41 | ); 42 | let mut input = String::new(); 43 | io::stdin().read_line(&mut input)?; 44 | 45 | host.remove_actor("MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ")?; 46 | println!("Actor removed. Echo server should still be working. Press ENTER to finish"); 47 | io::stdin().read_line(&mut input)?; 48 | 49 | Ok(()) 50 | } 51 | 52 | fn redis_config() -> HashMap { 53 | let mut hm = HashMap::new(); 54 | hm.insert("URL".to_string(), "redis://127.0.0.1:6379".to_string()); 55 | 56 | hm 57 | } 58 | 59 | fn generate_port_config(port: u16) -> HashMap { 60 | let mut hm = HashMap::new(); 61 | hm.insert("PORT".to_string(), port.to_string()); 62 | 63 | hm 64 | } 65 | -------------------------------------------------------------------------------- /examples/subscriber.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use wascc_host::{Actor, Host, NativeCapability}; 3 | 4 | fn main() -> std::result::Result<(), Box> { 5 | env_logger::init(); 6 | let host = Host::new(); 7 | host.add_actor(Actor::from_file("./examples/.assets/subscriber.wasm")?)?; 8 | host.add_actor(Actor::from_file("./examples/.assets/subscriber2.wasm")?)?; 9 | host.add_native_capability(NativeCapability::from_file( 10 | "./examples/.assets/libwascc_nats.so", 11 | None, 12 | )?)?; 13 | 14 | host.set_binding( 15 | "MBHRSJORBXAPRCALK6EKOBBCNAPMRTM6ODLXNLOV5TKPDMPXMTCMR4DW", 16 | "wascc:messaging", 17 | None, 18 | generate_config("test"), 19 | )?; 20 | 21 | host.set_binding( 22 | "MDJUPIQFWEHWE4XHPWHOJLW42SJDPVBQVDC2NV3T3O4ELXXVOXLA5M4I", 23 | "wascc:messaging", 24 | None, 25 | generate_config("second_test"), 26 | )?; 27 | std::thread::park(); 28 | 29 | Ok(()) 30 | } 31 | 32 | fn generate_config(sub: &str) -> HashMap { 33 | let mut hm = HashMap::new(); 34 | hm.insert("SUBSCRIPTION".to_string(), sub.to_string()); 35 | hm.insert("URL".to_string(), "nats://localhost:4222".to_string()); 36 | 37 | hm 38 | } 39 | -------------------------------------------------------------------------------- /src/actor.rs: -------------------------------------------------------------------------------- 1 | use crate::authz; 2 | use crate::Result; 3 | use std::fs::File; 4 | use std::io::prelude::*; 5 | use std::path::Path; 6 | use wascap::jwt::Token; 7 | 8 | /// An actor is a WebAssembly module that conforms to the waSCC protocols and can securely 9 | /// consume capabilities exposed by native or portable capability providers 10 | #[derive(Debug)] 11 | pub struct Actor { 12 | pub(crate) token: Token, 13 | pub(crate) bytes: Vec, 14 | } 15 | 16 | impl Actor { 17 | /// Create an actor from the bytes of a signed WebAssembly module. Attempting to load 18 | /// an unsigned module, or a module signed improperly, will result in an error 19 | pub fn from_slice(buf: &[u8]) -> Result { 20 | let token = authz::extract_claims(&buf)?; 21 | Ok(Actor { 22 | token, 23 | bytes: buf.to_vec(), 24 | }) 25 | } 26 | 27 | /// Create an actor from a signed WebAssembly (`.wasm`) file 28 | pub fn from_file(path: impl AsRef) -> Result { 29 | let mut file = File::open(path)?; 30 | let mut buf = Vec::new(); 31 | file.read_to_end(&mut buf)?; 32 | 33 | Actor::from_slice(&buf) 34 | } 35 | 36 | /// Obtain the actor's public key (The `sub` field of a JWT). This can be treated as a globally unique identifier 37 | pub fn public_key(&self) -> String { 38 | self.token.claims.subject.to_string() 39 | } 40 | 41 | /// The actor's human-friendly display name 42 | pub fn name(&self) -> String { 43 | match self.token.claims.metadata.as_ref().unwrap().name { 44 | Some(ref n) => n.to_string(), 45 | None => "Unnamed".to_string(), 46 | } 47 | } 48 | 49 | /// Obtain the public key of the issuer of the actor's signed token (the `iss` field of the JWT) 50 | pub fn issuer(&self) -> String { 51 | self.token.claims.issuer.to_string() 52 | } 53 | 54 | /// Obtain the list of capabilities declared in this actor's embedded token 55 | pub fn capabilities(&self) -> Vec { 56 | match self.token.claims.metadata.as_ref().unwrap().caps { 57 | Some(ref caps) => caps.clone(), 58 | None => vec![], 59 | } 60 | } 61 | 62 | /// Obtain the list of tags in the actor's token 63 | pub fn tags(&self) -> Vec { 64 | match self.token.claims.metadata.as_ref().unwrap().tags { 65 | Some(ref tags) => tags.clone(), 66 | None => vec![], 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/authz.rs: -------------------------------------------------------------------------------- 1 | use crate::errors; 2 | use crate::{Host, Result, WasccEntity}; 3 | use std::collections::HashMap; 4 | use std::sync::Arc; 5 | use std::sync::RwLock; 6 | use wascap::jwt::Token; 7 | use wascap::prelude::*; 8 | 9 | pub(crate) type ClaimsMap = Arc>>>; 10 | 11 | /// An authorizer is responsible for determining whether an actor can be loaded as well as 12 | /// whether an actor can invoke another entity. For invocation checks, the authorizer is only ever invoked _after_ 13 | /// an initial capability attestation check has been performed and _passed_. This has the net effect of making it 14 | /// impossible to override the base behavior of checking that an actor's embedded JWT contains the right 15 | /// capability attestations. 16 | pub trait Authorizer: Sync + Send { 17 | /// This check is performed during the `add_actor` call, allowing the custom authorizer to do things 18 | /// like verify a provenance chain, make external calls, etc. 19 | fn can_load(&self, claims: &Claims) -> bool; 20 | /// This check will be performed for _every_ invocation that has passed the base capability check, 21 | /// including the operation that occurs during `bind_actor`. Developers should be aware of this because 22 | /// if `set_authorizer` is done _after_ actor binding, it could potentially allow an unauthorized binding. 23 | fn can_invoke(&self, claims: &Claims, target: &WasccEntity, operation: &str) -> bool; 24 | } 25 | 26 | pub(crate) struct DefaultAuthorizer {} 27 | 28 | impl DefaultAuthorizer { 29 | pub fn new() -> impl Authorizer { 30 | DefaultAuthorizer {} 31 | } 32 | } 33 | 34 | impl Authorizer for DefaultAuthorizer { 35 | fn can_load(&self, _claims: &Claims) -> bool { 36 | true 37 | } 38 | 39 | // This doesn't actually mean everyone can invoke everything. Remember that the host itself 40 | // will _always_ enforce the claims check on an actor having the required capability 41 | // attestation 42 | fn can_invoke(&self, _claims: &Claims, target: &WasccEntity, _operation: &str) -> bool { 43 | match target { 44 | WasccEntity::Actor(_a) => true, 45 | WasccEntity::Capability { .. } => true, 46 | } 47 | } 48 | } 49 | 50 | pub(crate) fn get_all_claims(map: ClaimsMap) -> Vec<(String, Claims)> { 51 | map.read() 52 | .unwrap() 53 | .iter() 54 | .map(|(pk, claims)| (pk.clone(), claims.clone())) 55 | .collect() 56 | } 57 | 58 | // We don't (yet) support per-operation security constraints, but when we do, this 59 | // function will be ready to support that without breaking everyone else's calls 60 | pub(crate) fn can_invoke( 61 | claims: &Claims, 62 | capability_id: &str, 63 | _operation: &str, 64 | ) -> bool { 65 | // Edge case - deliver configuration to an actor directly, 66 | // so "self invocation" needs to be authorized 67 | if claims.subject == capability_id { 68 | return true; 69 | } 70 | claims 71 | .metadata 72 | .as_ref() 73 | .unwrap() 74 | .caps 75 | .as_ref() 76 | .map_or(false, |caps| caps.contains(&capability_id.to_string())) 77 | } 78 | 79 | // Extract claims from the JWT embedded in the wasm module's custom section 80 | pub(crate) fn extract_claims(buf: &[u8]) -> Result> { 81 | let token = wascap::wasm::extract_claims(buf)?; 82 | match token { 83 | Some(token) => { 84 | let claims = token.claims.clone(); 85 | let caps = claims.metadata.as_ref().unwrap().caps.clone(); 86 | info!( 87 | "Actor claims loaded for {} - {}", 88 | &claims.subject, 89 | caps.unwrap_or(vec![]).join(",") 90 | ); 91 | Ok(token) 92 | } 93 | None => Err(errors::new(errors::ErrorKind::Authorization( 94 | "No embedded JWT in actor module".to_string(), 95 | ))), 96 | } 97 | } 98 | 99 | pub(crate) fn enforce_validation(jwt: &str) -> Result<()> { 100 | let v = validate_token::(jwt)?; 101 | if v.expired { 102 | Err(errors::new(errors::ErrorKind::Authorization( 103 | "Expired token".to_string(), 104 | ))) 105 | } else if v.cannot_use_yet { 106 | Err(errors::new(errors::ErrorKind::Authorization(format!( 107 | "Module cannot be used before {}", 108 | v.not_before_human 109 | )))) 110 | } else { 111 | Ok(()) 112 | } 113 | } 114 | 115 | pub(crate) fn register_claims( 116 | claims_map: ClaimsMap, 117 | subject: &str, 118 | claims: Claims, 119 | ) { 120 | claims_map 121 | .write() 122 | .unwrap() 123 | .insert(subject.to_string(), claims); 124 | } 125 | 126 | pub(crate) fn unregister_claims(claims_map: ClaimsMap, subject: &str) { 127 | { 128 | let mut lock = claims_map.write().unwrap(); 129 | let _ = lock.remove(subject); 130 | } 131 | } 132 | 133 | impl Host { 134 | pub(crate) fn check_auth(&self, token: &Token) -> bool { 135 | self.authorizer.read().unwrap().can_load(&token.claims) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/bin.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::time::Duration; 3 | use structopt::clap::AppSettings; 4 | use structopt::StructOpt; 5 | use wascc_host::{Host, HostBuilder, HostManifest}; 6 | 7 | #[macro_use] 8 | extern crate log; 9 | 10 | #[derive(Debug, StructOpt, Clone)] 11 | #[structopt( 12 | global_settings(& [AppSettings::ColoredHelp, AppSettings::VersionlessSubcommands]), 13 | name = "wascc-host", 14 | about = "A general-purpose waSCC runtime host")] 15 | struct Cli { 16 | #[structopt(flatten)] 17 | command: CliCommand, 18 | } 19 | 20 | #[derive(Debug, Clone, StructOpt)] 21 | struct CliCommand { 22 | /// Path to the host manifest 23 | #[structopt(short = "m", long = "manifest", parse(from_os_str))] 24 | manifest_path: Option, 25 | /// Whether to expand environment variables in the host manifest 26 | #[structopt(short = "e", long = "expand-env")] 27 | expand_env: bool, 28 | } 29 | 30 | #[cfg(feature = "manifest")] 31 | fn main() -> std::result::Result<(), Box> { 32 | let args = Cli::from_args(); 33 | let cmd = args.command; 34 | let _ = env_logger::Builder::from_env( 35 | env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "wascc_host=info"), 36 | ) 37 | .format_module_path(false) 38 | .try_init(); 39 | 40 | let host = HostBuilder::new().build(); 41 | 42 | if let Some(ref mp) = cmd.manifest_path { 43 | let manifest = HostManifest::from_path(mp, cmd.expand_env)?; 44 | host.apply_manifest(manifest)?; 45 | info!("Processed and applied host manifest"); 46 | } else { 47 | info!("Starting without manifest"); 48 | #[cfg(not(feature = "lattice"))] 49 | { 50 | error!("Started without manifest and without lattice. This host cannot launch actors or providers. Shutting down."); 51 | return Err("Started without manifest or lattice - unusable host".into()); 52 | } 53 | } 54 | 55 | let (term_s, term_r) = std::sync::mpsc::channel(); 56 | 57 | ctrlc::set_handler(move || { 58 | term_s.send(()).unwrap(); 59 | }) 60 | .expect("Error setting Ctrl-C handler"); 61 | 62 | term_r.recv().expect("Failed awaiting termination signal"); 63 | 64 | info!("Shutting down host"); 65 | host.shutdown()?; 66 | 67 | Ok(()) 68 | } 69 | 70 | #[cfg(not(feature = "manifest"))] 71 | fn main() -> std::result::Result<(), Box> { 72 | println!( 73 | "The general-purpose waSCC host application needs to be built with the `manifest` feature." 74 | ); 75 | Ok(()) 76 | } 77 | -------------------------------------------------------------------------------- /src/bus/inproc.rs: -------------------------------------------------------------------------------- 1 | use crate::errors; 2 | use crate::{Invocation, InvocationResponse, Result}; 3 | use crossbeam::{Receiver, Sender}; 4 | use std::{collections::HashMap, sync::RwLock}; 5 | 6 | pub(crate) struct InprocBus { 7 | subscriptions: RwLock, Receiver)>>, 8 | } 9 | 10 | impl InprocBus { 11 | pub fn new() -> Self { 12 | info!("Initialized Message Bus (internal)"); 13 | InprocBus { 14 | subscriptions: RwLock::new(HashMap::new()), 15 | } 16 | } 17 | 18 | pub fn disconnect(&self) { 19 | // No-op 20 | } 21 | 22 | pub fn subscribe( 23 | &self, 24 | subject: &str, 25 | sender: crossbeam::Sender, 26 | receiver: crossbeam::Receiver, 27 | ) -> Result<()> { 28 | self.subscriptions 29 | .write() 30 | .unwrap() 31 | .insert(subject.to_string(), (sender, receiver)); 32 | Ok(()) 33 | } 34 | 35 | pub fn nqsubscribe( 36 | &self, 37 | subject: &str, 38 | sender: crossbeam::Sender, 39 | receiver: crossbeam::Receiver, 40 | ) -> Result<()> { 41 | self.subscribe(subject, sender, receiver) 42 | } 43 | 44 | pub fn invoke(&self, subject: &str, inv: Invocation) -> Result { 45 | match self.subscriptions.read().unwrap().get(subject) { 46 | Some(s) => { 47 | s.0.send(inv).unwrap(); 48 | let r = s.1.recv().unwrap(); 49 | Ok(r) 50 | } 51 | None => Err(errors::new(errors::ErrorKind::MiscHost(format!( 52 | "Attempted bus call for {} with no subscribers", 53 | subject 54 | )))), 55 | } 56 | } 57 | 58 | pub fn unsubscribe(&self, subject: &str) -> Result<()> { 59 | self.subscriptions 60 | .write() 61 | .unwrap() 62 | .remove(&subject.to_string()); 63 | Ok(()) 64 | } 65 | 66 | pub fn actor_subject(&self, actor: &str) -> String { 67 | super::actor_subject(None, actor) 68 | } 69 | 70 | pub(crate) fn provider_subject(&self, capid: &str, binding: &str) -> String { 71 | super::provider_subject(None, capid, binding) 72 | } 73 | 74 | pub(crate) fn inventory_wildcard_subject(&self) -> String { 75 | super::inventory_wildcard_subject(None) 76 | } 77 | 78 | pub(crate) fn event_subject(&self) -> String { 79 | super::event_subject(None) 80 | } 81 | 82 | pub(crate) fn provider_subject_bound_actor( 83 | &self, 84 | capid: &str, 85 | binding: &str, 86 | calling_actor: &str, 87 | ) -> String { 88 | super::provider_subject_bound_actor(None, capid, binding, calling_actor) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/bus/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "lattice")] 2 | use crossbeam::Sender; 3 | 4 | pub const URL_SCHEME: &str = "wasmbus"; 5 | 6 | #[cfg(feature = "lattice")] 7 | use crate::{BindingsList, RouteKey}; 8 | #[cfg(feature = "lattice")] 9 | use std::collections::HashMap; 10 | #[cfg(feature = "lattice")] 11 | use std::sync::{Arc, RwLock}; 12 | #[cfg(feature = "lattice")] 13 | use wascap::jwt::{Actor, Claims}; 14 | #[cfg(feature = "lattice")] 15 | use wascc_codec::capabilities::CapabilityDescriptor; 16 | 17 | #[cfg(not(feature = "lattice"))] 18 | pub(crate) mod inproc; 19 | #[cfg(feature = "lattice")] 20 | pub(crate) mod lattice; 21 | 22 | #[cfg(not(feature = "lattice"))] 23 | pub(crate) use inproc::InprocBus as MessageBus; 24 | 25 | #[cfg(feature = "lattice")] 26 | pub(crate) use lattice::DistributedBus as MessageBus; 27 | 28 | #[cfg(not(feature = "lattice"))] 29 | pub(crate) fn new() -> MessageBus { 30 | inproc::InprocBus::new() 31 | } 32 | 33 | #[cfg(feature = "lattice")] 34 | pub(crate) fn new( 35 | host_id: String, 36 | claims: Arc>>>, 37 | caps: Arc>>, 38 | bindings: Arc>, 39 | labels: Arc>>, 40 | terminators: Arc>>>, 41 | ns: Option, 42 | cplane_s: Sender, 43 | authz: Arc>>, 44 | image_map: Arc>>, 45 | ) -> MessageBus { 46 | lattice::DistributedBus::new( 47 | host_id, 48 | claims, 49 | caps, 50 | bindings, 51 | labels, 52 | terminators, 53 | ns, 54 | cplane_s, 55 | authz, 56 | image_map, 57 | ) 58 | } 59 | 60 | const LATTICE_NAMESPACE_ENV: &str = "LATTICE_NAMESPACE"; 61 | 62 | pub(crate) fn get_namespace_prefix() -> Option { 63 | ::std::env::var(LATTICE_NAMESPACE_ENV).ok() 64 | } 65 | 66 | pub(crate) fn actor_subject(ns: Option<&str>, actor: &str) -> String { 67 | format!("{}.actor.{}", nsprefix(ns), actor) 68 | } 69 | 70 | pub(crate) fn provider_subject(ns: Option<&str>, capid: &str, binding: &str) -> String { 71 | format!( 72 | "{}.provider.{}.{}", 73 | nsprefix(ns), 74 | normalize_capid(capid), 75 | binding 76 | ) 77 | } 78 | 79 | pub(crate) fn inventory_wildcard_subject(ns: Option<&str>) -> String { 80 | format!("{}.inventory.*", nsprefix(ns)) 81 | } 82 | 83 | pub(crate) fn event_subject(ns: Option<&str>) -> String { 84 | format!("{}.events", nsprefix(ns)) 85 | } 86 | 87 | // By convention most of the waSCC ecosystem uses a "group:item" string 88 | // for the capability IDs, e.g. "wascc:messaging" or "gpio:relay". To 89 | // accommodate message broker subjects that might not work with the ":" 90 | // character, we normalize the segments to dot-separated. 91 | pub(crate) fn normalize_capid(capid: &str) -> String { 92 | capid.to_lowercase().replace(":", ".").replace(" ", "_") 93 | } 94 | 95 | pub(crate) fn provider_subject_bound_actor( 96 | ns: Option<&str>, 97 | capid: &str, 98 | binding: &str, 99 | calling_actor: &str, 100 | ) -> String { 101 | format!( 102 | "{}.provider.{}.{}.{}", 103 | nsprefix(ns), 104 | normalize_capid(capid), 105 | binding, 106 | calling_actor 107 | ) 108 | } 109 | 110 | pub(crate) fn nsprefix(ns: Option<&str>) -> String { 111 | match ns { 112 | Some(s) => format!("{}.wasmbus", s), 113 | None => "wasmbus".to_string(), 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/capability.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | use libloading::Library; 3 | use libloading::Symbol; 4 | use std::ffi::OsStr; 5 | use wascc_codec::{ 6 | capabilities::{CapabilityDescriptor, CapabilityProvider, OP_GET_CAPABILITY_DESCRIPTOR}, 7 | deserialize, SYSTEM_ACTOR, 8 | }; 9 | 10 | /// Represents a native capability provider compiled as a shared object library. 11 | /// These plugins are OS- and architecture-specific, so they will be `.so` files on Linux, `.dylib` 12 | /// files on macOS, etc. 13 | pub struct NativeCapability { 14 | pub(crate) plugin: Box, 15 | pub(crate) binding_name: String, 16 | pub(crate) descriptor: CapabilityDescriptor, 17 | // This field is solely used to keep the FFI library instance allocated for the same 18 | // lifetime as the boxed plugin 19 | #[allow(dead_code)] 20 | library: Option, 21 | } 22 | 23 | impl NativeCapability { 24 | /// Reads a capability provider from a file. The capability provider must implement the 25 | /// correct FFI interface to support waSCC plugins. See [wascc.dev](https://wascc.dev) for 26 | /// documentation and tutorials on how to create a native capability provider 27 | pub fn from_file>( 28 | filename: P, 29 | binding_target_name: Option, 30 | ) -> Result { 31 | type PluginCreate = unsafe fn() -> *mut dyn CapabilityProvider; 32 | 33 | let library = Library::new(filename.as_ref())?; 34 | 35 | let plugin = unsafe { 36 | let constructor: Symbol = library.get(b"__capability_provider_create")?; 37 | let boxed_raw = constructor(); 38 | 39 | Box::from_raw(boxed_raw) 40 | }; 41 | let descriptor = get_descriptor(&plugin)?; 42 | let binding = binding_target_name.unwrap_or("default".to_string()); 43 | info!( 44 | "Loaded native capability provider '{}' v{} ({}) for {}/{}", 45 | descriptor.name, descriptor.version, descriptor.revision, descriptor.id, binding 46 | ); 47 | 48 | Ok(NativeCapability { 49 | plugin, 50 | descriptor, 51 | binding_name: binding, 52 | library: Some(library), 53 | }) 54 | } 55 | 56 | /// This function is to be used for _capability embedding_. If you are building a custom 57 | /// waSCC host and have a fixed set of capabilities that you want to always be available 58 | /// to actors, then you can declare a dependency on the capability provider, enable 59 | /// the `static_plugin` feature, and provide an instance of that provider. Be sure to check 60 | /// that the provider supports capability embedding. 61 | pub fn from_instance( 62 | instance: impl CapabilityProvider, 63 | binding_target_name: Option, 64 | ) -> Result { 65 | let b: Box = Box::new(instance); 66 | let descriptor = get_descriptor(&b)?; 67 | let binding = binding_target_name.unwrap_or("default".to_string()); 68 | 69 | info!( 70 | "Loaded native capability provider '{}' v{} ({}) for {}/{}", 71 | descriptor.name, descriptor.version, descriptor.revision, descriptor.id, binding 72 | ); 73 | Ok(NativeCapability { 74 | descriptor, 75 | plugin: b, 76 | binding_name: binding, 77 | library: None, 78 | }) 79 | } 80 | 81 | /// Returns the capability ID (namespace) of the provider 82 | pub fn id(&self) -> String { 83 | self.descriptor.id.to_string() 84 | } 85 | 86 | /// Returns the human-friendly name of the provider 87 | pub fn name(&self) -> String { 88 | self.descriptor.name.to_string() 89 | } 90 | 91 | /// Returns the full descriptor for the capability provider 92 | pub fn descriptor(&self) -> &CapabilityDescriptor { 93 | &self.descriptor 94 | } 95 | } 96 | 97 | fn get_descriptor(plugin: &Box) -> Result { 98 | let res = plugin.handle_call(SYSTEM_ACTOR, OP_GET_CAPABILITY_DESCRIPTOR, &[])?; 99 | let descriptor: CapabilityDescriptor = deserialize(&res)?; 100 | Ok(descriptor) 101 | } 102 | -------------------------------------------------------------------------------- /src/dispatch.rs: -------------------------------------------------------------------------------- 1 | use crate::bus::MessageBus; 2 | use crate::inthost::{Invocation, WasccEntity}; 3 | use std::{error::Error, sync::Arc}; 4 | 5 | use wascap::prelude::KeyPair; 6 | use wascc_codec::capabilities::Dispatcher; 7 | 8 | /// A dispatcher is given to each capability provider, allowing it to send 9 | /// commands in to the guest module and await replies. This dispatch 10 | /// is one way, and is _not_ used for the guest module to send commands to capabilities 11 | #[derive(Clone)] 12 | pub(crate) struct WasccNativeDispatcher { 13 | bus: Arc, 14 | capid: String, 15 | binding: String, 16 | hk: Arc, 17 | } 18 | 19 | impl WasccNativeDispatcher { 20 | pub fn new(hk: Arc, bus: Arc, capid: &str, binding: &str) -> Self { 21 | WasccNativeDispatcher { 22 | bus, 23 | capid: capid.to_string(), 24 | binding: binding.to_string(), 25 | hk, 26 | } 27 | } 28 | } 29 | 30 | impl Dispatcher for WasccNativeDispatcher { 31 | /// Called by a capability provider to invoke a function on an actor 32 | fn dispatch( 33 | &self, 34 | actor: &str, 35 | op: &str, 36 | msg: &[u8], 37 | ) -> Result, Box> { 38 | trace!( 39 | "Dispatching operation '{}' ({} bytes) to actor", 40 | op, 41 | msg.len() 42 | ); 43 | let inv = Invocation::new( 44 | &self.hk, 45 | WasccEntity::Capability { 46 | capid: self.capid.to_string(), 47 | binding: self.binding.to_string(), 48 | }, 49 | WasccEntity::Actor(actor.to_string()), 50 | op, 51 | msg.to_vec(), 52 | ); 53 | let tgt_sub = self.bus.actor_subject(actor); 54 | let resp = self.bus.invoke(&tgt_sub, inv); 55 | 56 | match resp { 57 | Ok(r) => match r.error { 58 | Some(e) => Err(format!("Invocation failure: {}", e).into()), 59 | None => Ok(r.msg), 60 | }, 61 | Err(e) => Err(Box::new(e)), 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | //! Custom error types 2 | use std::error::Error as StdError; 3 | use std::fmt; 4 | 5 | #[derive(Debug)] 6 | pub struct Error(Box); 7 | 8 | pub fn new(kind: ErrorKind) -> Error { 9 | Error(Box::new(kind)) 10 | } 11 | 12 | #[derive(Debug)] 13 | pub enum ErrorKind { 14 | Wapc(wapc::errors::Error), 15 | HostCallFailure(Box), 16 | Wascap(wascap::Error), 17 | Authorization(String), 18 | IO(std::io::Error), 19 | CapabilityProvider(String), 20 | MiscHost(String), 21 | Plugin(libloading::Error), 22 | Middleware(String), 23 | Serialization(String), 24 | } 25 | 26 | impl Error { 27 | pub fn kind(&self) -> &ErrorKind { 28 | &self.0 29 | } 30 | 31 | pub fn into_kind(self) -> ErrorKind { 32 | *self.0 33 | } 34 | } 35 | 36 | impl StdError for Error { 37 | fn description(&self) -> &str { 38 | match *self.0 { 39 | ErrorKind::Wapc(_) => "waPC error", 40 | ErrorKind::IO(_) => "I/O error", 41 | ErrorKind::HostCallFailure(_) => "Error occurred during host call", 42 | ErrorKind::Wascap(_) => "Embedded JWT Failure", 43 | ErrorKind::Authorization(_) => "Module authorization failure", 44 | ErrorKind::CapabilityProvider(_) => "Capability provider failure", 45 | ErrorKind::MiscHost(_) => "waSCC Host error", 46 | ErrorKind::Plugin(_) => "Plugin error", 47 | ErrorKind::Middleware(_) => "Middleware error", 48 | ErrorKind::Serialization(_) => "Serialization failure", 49 | } 50 | } 51 | 52 | fn cause(&self) -> Option<&dyn StdError> { 53 | match *self.0 { 54 | ErrorKind::Wapc(ref err) => Some(err), 55 | ErrorKind::HostCallFailure(_) => None, 56 | ErrorKind::Wascap(ref err) => Some(err), 57 | ErrorKind::Authorization(_) => None, 58 | ErrorKind::IO(ref err) => Some(err), 59 | ErrorKind::CapabilityProvider(_) => None, 60 | ErrorKind::MiscHost(_) => None, 61 | ErrorKind::Plugin(ref err) => Some(err), 62 | ErrorKind::Middleware(_) => None, 63 | ErrorKind::Serialization(_) => None, 64 | } 65 | } 66 | } 67 | 68 | impl fmt::Display for Error { 69 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 70 | match *self.0 { 71 | ErrorKind::Wapc(ref err) => write!(f, "waPC failure: {}", err), 72 | ErrorKind::HostCallFailure(ref err) => { 73 | write!(f, "Error occurred during host call: {}", err) 74 | } 75 | ErrorKind::Wascap(ref err) => write!(f, "Embedded JWT failure: {}", err), 76 | ErrorKind::Authorization(ref err) => { 77 | write!(f, "WebAssembly module authorization failure: {}", err) 78 | } 79 | ErrorKind::IO(ref err) => write!(f, "I/O error: {}", err), 80 | ErrorKind::CapabilityProvider(ref err) => { 81 | write!(f, "Capability provider error: {}", err) 82 | } 83 | ErrorKind::MiscHost(ref err) => write!(f, "waSCC Host Error: {}", err), 84 | ErrorKind::Plugin(ref err) => write!(f, "Plugin error: {}", err), 85 | ErrorKind::Middleware(ref err) => write!(f, "Middleware error: {}", err), 86 | ErrorKind::Serialization(ref err) => write!(f, "Serialization failure: {}", err), 87 | } 88 | } 89 | } 90 | 91 | impl From for Error { 92 | fn from(source: libloading::Error) -> Error { 93 | Error(Box::new(ErrorKind::Plugin(source))) 94 | } 95 | } 96 | impl From for Error { 97 | fn from(source: wascap::Error) -> Error { 98 | Error(Box::new(ErrorKind::Wascap(source))) 99 | } 100 | } 101 | 102 | impl From for Error { 103 | fn from(source: wapc::errors::Error) -> Error { 104 | Error(Box::new(ErrorKind::Wapc(source))) 105 | } 106 | } 107 | 108 | impl From for Error { 109 | fn from(source: std::io::Error) -> Error { 110 | Error(Box::new(ErrorKind::IO(source))) 111 | } 112 | } 113 | 114 | impl From> for Error { 115 | fn from(source: Box) -> Error { 116 | Error(Box::new(ErrorKind::HostCallFailure(source))) 117 | } 118 | } 119 | 120 | impl From for Error { 121 | fn from(source: String) -> Error { 122 | Error(Box::new(ErrorKind::MiscHost(source))) 123 | } 124 | } 125 | 126 | #[cfg(test)] 127 | mod tests { 128 | #[allow(dead_code)] 129 | fn assert_sync_send() {} 130 | const _: fn() = || assert_sync_send::(); 131 | } 132 | -------------------------------------------------------------------------------- /src/extras.rs: -------------------------------------------------------------------------------- 1 | // A default implementation of the "wascc:extras" provider that is always included 2 | // with the host runtime. This provides functionality for generating random numbers, 3 | // generating a guid, and generating a sequence number... things that a standalone 4 | // WASM module cannot do. 5 | 6 | use crate::{REVISION, VERSION}; 7 | use std::error::Error; 8 | use std::sync::{Arc, RwLock}; 9 | use std::{ 10 | collections::HashMap, 11 | sync::atomic::{AtomicU64, Ordering}, 12 | }; 13 | use uuid::Uuid; 14 | use wascc_codec::capabilities::{ 15 | CapabilityDescriptor, CapabilityProvider, Dispatcher, NullDispatcher, OperationDirection, 16 | OP_GET_CAPABILITY_DESCRIPTOR, 17 | }; 18 | use wascc_codec::core::OP_BIND_ACTOR; 19 | use wascc_codec::extras::*; 20 | use wascc_codec::{deserialize, serialize, SYSTEM_ACTOR}; 21 | 22 | pub(crate) struct ExtrasCapabilityProvider { 23 | dispatcher: Arc>>, 24 | sequences: Arc>>, 25 | } 26 | 27 | impl Default for ExtrasCapabilityProvider { 28 | fn default() -> Self { 29 | ExtrasCapabilityProvider { 30 | dispatcher: Arc::new(RwLock::new(Box::new(NullDispatcher::new()))), 31 | sequences: Arc::new(RwLock::new(HashMap::new())), 32 | } 33 | } 34 | } 35 | 36 | pub(crate) const CAPABILITY_ID: &str = "wascc:extras"; 37 | 38 | impl ExtrasCapabilityProvider { 39 | fn generate_guid( 40 | &self, 41 | _actor: &str, 42 | _msg: GeneratorRequest, 43 | ) -> Result, Box> { 44 | let uuid = Uuid::new_v4(); 45 | let result = GeneratorResult { 46 | guid: Some(format!("{}", uuid)), 47 | random_number: 0, 48 | sequence_number: 0, 49 | }; 50 | 51 | Ok(serialize(&result)?) 52 | } 53 | 54 | fn generate_random( 55 | &self, 56 | _actor: &str, 57 | msg: GeneratorRequest, 58 | ) -> Result, Box> { 59 | use rand::prelude::*; 60 | let mut rng = rand::thread_rng(); 61 | let result = if let GeneratorRequest { 62 | random: true, 63 | min, 64 | max, 65 | .. 66 | } = msg 67 | { 68 | let n: u32 = rng.gen_range(min, max); 69 | GeneratorResult { 70 | random_number: n, 71 | sequence_number: 0, 72 | guid: None, 73 | } 74 | } else { 75 | GeneratorResult::default() 76 | }; 77 | 78 | Ok(serialize(result)?) 79 | } 80 | 81 | fn generate_sequence( 82 | &self, 83 | actor: &str, 84 | _msg: GeneratorRequest, 85 | ) -> Result, Box> { 86 | let mut lock = self.sequences.write().unwrap(); 87 | let seq = lock 88 | .entry(actor.to_string()) 89 | .or_insert(AtomicU64::new(0)) 90 | .fetch_add(1, Ordering::SeqCst); 91 | let result = GeneratorResult { 92 | sequence_number: seq, 93 | random_number: 0, 94 | guid: None, 95 | }; 96 | Ok(serialize(&result)?) 97 | } 98 | 99 | fn get_descriptor(&self) -> Result, Box> { 100 | Ok(serialize( 101 | CapabilityDescriptor::builder() 102 | .id(CAPABILITY_ID) 103 | .name("waSCC Extras (Internal)") 104 | .long_description( 105 | "A capability provider exposing miscellaneous utility functions to actors", 106 | ) 107 | .version(VERSION) 108 | .revision(REVISION) 109 | .with_operation( 110 | OP_REQUEST_GUID, 111 | OperationDirection::ToProvider, 112 | "Requests the generation of a new GUID", 113 | ) 114 | .with_operation( 115 | OP_REQUEST_RANDOM, 116 | OperationDirection::ToProvider, 117 | "Requests the generation of a randum number", 118 | ) 119 | .with_operation( 120 | OP_REQUEST_SEQUENCE, 121 | OperationDirection::ToProvider, 122 | "Requests the next number in a process-wide global sequence number", 123 | ) 124 | .build(), 125 | )?) 126 | } 127 | } 128 | 129 | impl CapabilityProvider for ExtrasCapabilityProvider { 130 | fn configure_dispatch( 131 | &self, 132 | dispatcher: Box, 133 | ) -> Result<(), Box> { 134 | trace!("Dispatcher received."); 135 | let mut lock = self.dispatcher.write().unwrap(); 136 | *lock = dispatcher; 137 | 138 | Ok(()) 139 | } 140 | 141 | fn handle_call( 142 | &self, 143 | actor: &str, 144 | op: &str, 145 | msg: &[u8], 146 | ) -> Result, Box> { 147 | trace!("Received host call from {}, operation - {}", actor, op); 148 | 149 | match op { 150 | OP_GET_CAPABILITY_DESCRIPTOR if actor == SYSTEM_ACTOR => self.get_descriptor(), 151 | OP_REQUEST_GUID => self.generate_guid(actor, deserialize(msg)?), 152 | OP_REQUEST_RANDOM => self.generate_random(actor, deserialize(msg)?), 153 | OP_REQUEST_SEQUENCE => self.generate_sequence(actor, deserialize(msg)?), 154 | OP_BIND_ACTOR => Ok(vec![]), 155 | _ => Err("bad dispatch".into()), 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/inthost.rs: -------------------------------------------------------------------------------- 1 | // Implementations of support functions for the `Host` struct 2 | 3 | use super::Host; 4 | use crate::Result; 5 | use data_encoding::HEXUPPER; 6 | use ring::digest::{Context, Digest, SHA256}; 7 | 8 | use crate::bus; 9 | use crate::bus::MessageBus; 10 | use crate::BindingsList; 11 | use crate::{authz, errors, Actor, Authorizer, NativeCapability, RouteKey}; 12 | use errors::ErrorKind; 13 | use provider_archive::ProviderArchive; 14 | use std::str::FromStr; 15 | use std::{ 16 | collections::HashMap, 17 | io::Read, 18 | sync::{Arc, RwLock}, 19 | }; 20 | use uuid::Uuid; 21 | use wapc::WapcHost; 22 | use wascap::{jwt::Claims, prelude::KeyPair}; 23 | use wascc_codec::{ 24 | capabilities::{CapabilityDescriptor, OP_GET_CAPABILITY_DESCRIPTOR}, 25 | core::{CapabilityConfiguration, OP_PERFORM_LIVE_UPDATE, OP_REMOVE_ACTOR}, 26 | deserialize, serialize, SYSTEM_ACTOR, 27 | }; 28 | 29 | pub(crate) const CORELABEL_ARCH: &str = "hostcore.arch"; 30 | pub(crate) const CORELABEL_OS: &str = "hostcore.os"; 31 | pub(crate) const CORELABEL_OSFAMILY: &str = "hostcore.osfamily"; 32 | 33 | pub(crate) const OCI_VAR_USER: &str = "OCI_REGISTRY_USER"; 34 | pub(crate) const OCI_VAR_PASSWORD: &str = "OCI_REGISTRY_PASSWORD"; 35 | 36 | #[allow(dead_code)] 37 | pub(crate) const RESTRICTED_LABELS: [&str; 3] = [CORELABEL_OSFAMILY, CORELABEL_ARCH, CORELABEL_OS]; 38 | 39 | // Unsubscribes all of the private actor-provider comms subjects 40 | pub(crate) fn unsub_all_bindings( 41 | bindings: Arc>, 42 | bus: Arc, 43 | capid: &str, 44 | ) { 45 | bindings 46 | .read() 47 | .unwrap() 48 | .keys() 49 | .filter(|(_a, c, _b)| c == capid) 50 | .for_each(|(a, c, b)| { 51 | let _ = bus.unsubscribe(&bus.provider_subject_bound_actor(c, b, a)); 52 | }); 53 | } 54 | 55 | impl Host { 56 | pub(crate) fn record_binding( 57 | &self, 58 | actor: &str, 59 | capid: &str, 60 | binding: &str, 61 | config: &CapabilityConfiguration, 62 | ) -> Result<()> { 63 | let mut lock = self.bindings.write().unwrap(); 64 | lock.insert( 65 | (actor.to_string(), capid.to_string(), binding.to_string()), 66 | config.clone(), 67 | ); 68 | trace!( 69 | "Actor {} successfully bound to {},{}", 70 | actor, 71 | binding, 72 | capid 73 | ); 74 | Ok(()) 75 | } 76 | 77 | pub(crate) fn ensure_extras(&self) -> Result<()> { 78 | self.add_native_capability(NativeCapability::from_instance( 79 | crate::extras::ExtrasCapabilityProvider::default(), 80 | None, 81 | )?)?; 82 | Ok(()) 83 | } 84 | } 85 | 86 | /// In the case of a portable capability provider, obtain its capability descriptor 87 | pub(crate) fn get_descriptor(host: &mut WapcHost) -> Result { 88 | let msg = wascc_codec::core::HealthRequest { placeholder: false }; // TODO: eventually support sending an empty slice for this 89 | let res = host.call(OP_GET_CAPABILITY_DESCRIPTOR, &serialize(&msg)?)?; 90 | deserialize(&res).map_err(|e| e.into()) 91 | } 92 | 93 | pub(crate) fn remove_cap( 94 | caps: Arc>>, 95 | capid: &str, 96 | binding: &str, 97 | ) { 98 | caps.write().unwrap().remove(&RouteKey::new(binding, capid)); 99 | } 100 | 101 | /// Puts a "live update" message into the dispatch queue, which will be handled 102 | /// as soon as it is pulled off the channel for the target actor 103 | pub(crate) fn replace_actor( 104 | hostkey: &KeyPair, 105 | bus: Arc, 106 | new_actor: Actor, 107 | ) -> Result<()> { 108 | let public_key = new_actor.token.claims.subject; 109 | let tgt_subject = bus.actor_subject(&public_key); 110 | let inv = gen_liveupdate_invocation(hostkey, &public_key, new_actor.bytes); 111 | 112 | match bus.invoke(&tgt_subject, inv) { 113 | Ok(_) => { 114 | info!("Actor {} replaced", public_key); 115 | Ok(()) 116 | } 117 | Err(e) => Err(e), 118 | } 119 | } 120 | 121 | pub(crate) fn live_update(guest: &mut WapcHost, inv: &Invocation) -> InvocationResponse { 122 | match guest.replace_module(&inv.msg) { 123 | Ok(_) => InvocationResponse::success(inv, vec![]), 124 | Err(e) => { 125 | error!("Failed to perform hot swap, ignoring message: {}", e); 126 | InvocationResponse::error(inv, "Failed to perform hot swap") 127 | } 128 | } 129 | } 130 | 131 | fn gen_liveupdate_invocation(hostkey: &KeyPair, target: &str, bytes: Vec) -> Invocation { 132 | Invocation::new( 133 | hostkey, 134 | WasccEntity::Actor(SYSTEM_ACTOR.to_string()), 135 | WasccEntity::Actor(target.to_string()), 136 | OP_PERFORM_LIVE_UPDATE, 137 | bytes, 138 | ) 139 | } 140 | 141 | /// Removes all bindings for a given actor by sending the "remove actor" message 142 | /// to each of the capabilities 143 | pub(crate) fn deconfigure_actor( 144 | hostkey: KeyPair, 145 | bus: Arc, 146 | bindings: Arc>, 147 | key: &str, 148 | ) { 149 | #[cfg(feature = "lattice")] 150 | { 151 | // Don't remove the bindings for this actor unless it's the last instance in the lattice 152 | if let Ok(i) = bus.instance_count(key) { 153 | if i > 0 { 154 | // This is 0 because the actor being removed has already been taken out of the claims map, so bus queries will not see the local instance 155 | info!("Actor instance terminated at scale > 1, bypassing binding removal."); 156 | return; 157 | } 158 | } 159 | } 160 | let cfg = CapabilityConfiguration { 161 | module: key.to_string(), 162 | values: HashMap::new(), 163 | }; 164 | let buf = serialize(&cfg).unwrap(); 165 | let nbindings: Vec<_> = { 166 | let lock = bindings.read().unwrap(); 167 | lock.keys() 168 | .filter(|(a, _cap, _bind)| a == key) 169 | .cloned() 170 | .collect() 171 | }; 172 | 173 | // (actor, capid, binding) 174 | for (actor, capid, binding) in nbindings { 175 | info!("Unbinding actor {} from {},{}", actor, binding, capid); 176 | let _inv_r = bus.invoke( 177 | &bus.provider_subject(&capid, &binding), // The OP_REMOVE_ACTOR invocation should go to _all_ instances of the provider being unbound 178 | gen_remove_actor(&hostkey, buf.clone(), &binding, &capid), 179 | ); 180 | remove_binding(bindings.clone(), key, &binding, &capid); 181 | } 182 | } 183 | 184 | /// Removes all bindings from a capability without notifying anyone 185 | pub(crate) fn unbind_all_from_cap(bindings: Arc>, capid: &str, binding: &str) { 186 | let mut lock = bindings.write().unwrap(); 187 | // (actor, capid, binding name) 188 | lock.retain(|k, _| !(k.1 == capid) && (k.2 == binding)); 189 | } 190 | 191 | pub(crate) fn remove_binding( 192 | bindings: Arc>, 193 | actor: &str, 194 | binding: &str, 195 | capid: &str, 196 | ) { 197 | // binding: (actor, capid, binding) 198 | let mut lock = bindings.write().unwrap(); 199 | lock.remove(&(actor.to_string(), capid.to_string(), binding.to_string())); 200 | } 201 | 202 | pub(crate) fn gen_remove_actor( 203 | hostkey: &KeyPair, 204 | msg: Vec, 205 | binding: &str, 206 | capid: &str, 207 | ) -> Invocation { 208 | Invocation::new( 209 | hostkey, 210 | WasccEntity::Actor(SYSTEM_ACTOR.to_string()), 211 | WasccEntity::Capability { 212 | capid: capid.to_string(), 213 | binding: binding.to_string(), 214 | }, 215 | OP_REMOVE_ACTOR, 216 | msg, 217 | ) 218 | } 219 | 220 | /// An immutable representation of an invocation within waSCC 221 | #[derive(Debug, Clone)] 222 | #[cfg_attr(feature = "lattice", derive(serde::Serialize, serde::Deserialize))] 223 | pub struct Invocation { 224 | pub origin: WasccEntity, 225 | pub target: WasccEntity, 226 | pub operation: String, 227 | pub msg: Vec, 228 | pub id: String, 229 | pub encoded_claims: String, 230 | pub host_id: String, 231 | } 232 | 233 | /// Represents an invocation target - either an actor or a bound capability provider 234 | #[derive(Debug, Clone, PartialEq)] 235 | #[cfg_attr(feature = "lattice", derive(serde::Serialize, serde::Deserialize))] 236 | pub enum WasccEntity { 237 | Actor(String), 238 | Capability { capid: String, binding: String }, 239 | } 240 | 241 | impl WasccEntity { 242 | pub fn url(&self) -> String { 243 | match self { 244 | WasccEntity::Actor(pk) => format!("{}://{}", bus::URL_SCHEME, pk), 245 | WasccEntity::Capability { capid, binding } => format!( 246 | "{}://{}/{}", 247 | bus::URL_SCHEME, 248 | capid.replace(":", "/").replace(" ", "_").to_lowercase(), 249 | binding.replace(" ", "_").to_lowercase(), 250 | ), 251 | } 252 | } 253 | } 254 | 255 | impl Invocation { 256 | pub fn new( 257 | hostkey: &KeyPair, 258 | origin: WasccEntity, 259 | target: WasccEntity, 260 | op: &str, 261 | msg: Vec, 262 | ) -> Invocation { 263 | let subject = format!("{}", Uuid::new_v4()); 264 | let issuer = hostkey.public_key(); 265 | let target_url = format!("{}/{}", target.url(), op); 266 | let claims = Claims::::new( 267 | issuer.to_string(), 268 | subject.to_string(), 269 | &target_url, 270 | &origin.url(), 271 | &invocation_hash(&target_url, &origin.url(), &msg), 272 | ); 273 | Invocation { 274 | origin, 275 | target, 276 | operation: op.to_string(), 277 | msg, 278 | id: subject, 279 | encoded_claims: claims.encode(&hostkey).unwrap(), 280 | host_id: issuer.to_string(), 281 | } 282 | } 283 | 284 | pub fn origin_url(&self) -> String { 285 | self.origin.url() 286 | } 287 | 288 | pub fn target_url(&self) -> String { 289 | format!("{}/{}", self.target.url(), self.operation) 290 | } 291 | 292 | pub fn hash(&self) -> String { 293 | invocation_hash(&self.target_url(), &self.origin_url(), &self.msg) 294 | } 295 | 296 | pub fn validate_antiforgery(&self) -> Result<()> { 297 | let vr = wascap::jwt::validate_token::(&self.encoded_claims)?; 298 | let claims = Claims::::decode(&self.encoded_claims)?; 299 | if vr.expired { 300 | return Err(errors::new(ErrorKind::Authorization( 301 | "Invocation claims token expired".into(), 302 | ))); 303 | } 304 | if !vr.signature_valid { 305 | return Err(errors::new(ErrorKind::Authorization( 306 | "Invocation claims signature invalid".into(), 307 | ))); 308 | } 309 | if vr.cannot_use_yet { 310 | return Err(errors::new(ErrorKind::Authorization( 311 | "Attempt to use invocation before claims token allows".into(), 312 | ))); 313 | } 314 | let inv_claims = claims.metadata.unwrap(); 315 | if inv_claims.invocation_hash != self.hash() { 316 | return Err(errors::new(ErrorKind::Authorization( 317 | "Invocation hash does not match signed claims hash".into(), 318 | ))); 319 | } 320 | if claims.subject != self.id { 321 | return Err(errors::new(ErrorKind::Authorization( 322 | "Subject of invocation claims token does not match invocation ID".into(), 323 | ))); 324 | } 325 | if claims.issuer != self.host_id { 326 | return Err(errors::new(ErrorKind::Authorization( 327 | "Invocation claims issuer does not match invocation host".into(), 328 | ))); 329 | } 330 | if inv_claims.target_url != self.target_url() { 331 | return Err(errors::new(ErrorKind::Authorization( 332 | "Invocation claims and invocation target URL do not match".into(), 333 | ))); 334 | } 335 | if inv_claims.origin_url != self.origin_url() { 336 | return Err(errors::new(ErrorKind::Authorization( 337 | "Invocation claims and invocation origin URL do not match".into(), 338 | ))); 339 | } 340 | 341 | Ok(()) 342 | } 343 | } 344 | 345 | /// The response to an invocation 346 | #[derive(Debug, Clone)] 347 | #[cfg_attr(feature = "lattice", derive(serde::Serialize, serde::Deserialize))] 348 | pub struct InvocationResponse { 349 | pub msg: Vec, 350 | pub error: Option, 351 | pub invocation_id: String, 352 | } 353 | 354 | impl InvocationResponse { 355 | pub fn success(inv: &Invocation, msg: Vec) -> InvocationResponse { 356 | InvocationResponse { 357 | msg, 358 | error: None, 359 | invocation_id: inv.id.to_string(), 360 | } 361 | } 362 | 363 | pub fn error(inv: &Invocation, err: &str) -> InvocationResponse { 364 | InvocationResponse { 365 | msg: Vec::new(), 366 | error: Some(err.to_string()), 367 | invocation_id: inv.id.to_string(), 368 | } 369 | } 370 | } 371 | 372 | pub(crate) fn wapc_host_callback( 373 | hostkey: KeyPair, 374 | claims: Claims, 375 | bus: Arc, 376 | binding: &str, 377 | namespace: &str, 378 | operation: &str, 379 | payload: &[u8], 380 | authorizer: Arc>>, 381 | ) -> std::result::Result, Box> { 382 | trace!( 383 | "Guest {} invoking {}:{}", 384 | claims.subject, 385 | namespace, 386 | operation 387 | ); 388 | 389 | let capability_id = namespace; 390 | let inv = invocation_from_callback( 391 | &hostkey, 392 | &claims.subject, 393 | binding, 394 | namespace, 395 | operation, 396 | payload, 397 | ); 398 | 399 | if !authz::can_invoke(&claims, capability_id, operation) { 400 | return Err(Box::new(errors::new(errors::ErrorKind::Authorization( 401 | format!( 402 | "{} {} attempted to call {} on {},{} - PERMISSION DENIED.", 403 | if claims.metadata.unwrap().provider { 404 | "Provider" 405 | } else { 406 | "Actor" 407 | }, 408 | claims.subject, 409 | operation, 410 | capability_id, 411 | binding 412 | ), 413 | )))); 414 | } else { 415 | if !authorizer 416 | .read() 417 | .unwrap() 418 | .can_invoke(&claims, &inv.target, operation) 419 | { 420 | return Err(Box::new(errors::new(errors::ErrorKind::Authorization( 421 | format!( 422 | "{} {} attempted to call {:?} - Authorizer denied access", 423 | if claims.metadata.unwrap().provider { 424 | "Provider" 425 | } else { 426 | "Actor" 427 | }, 428 | claims.subject, 429 | &inv.target 430 | ), 431 | )))); 432 | } 433 | } 434 | // Make a request on either `wasmbus.Mxxxxx` for an actor or `wasmbus.{capid}.{binding}.{calling-actor}` for 435 | // a bound capability provider 436 | let invoke_subject = match &inv.target { 437 | WasccEntity::Actor(subject) => bus.actor_subject(subject), 438 | WasccEntity::Capability { capid, binding } => { 439 | bus.provider_subject_bound_actor(capid, binding, &claims.subject) 440 | } 441 | }; 442 | match bus.invoke(&invoke_subject, inv) { 443 | Ok(inv_r) => match inv_r.error { 444 | Some(e) => Err(format!("Invocation failure: {}", e).into()), 445 | None => Ok(inv_r.msg), 446 | }, 447 | Err(e) => Err(Box::new(errors::new(errors::ErrorKind::HostCallFailure( 448 | e.into(), 449 | )))), 450 | } 451 | } 452 | 453 | pub(crate) fn fetch_oci_bytes(img: &str) -> Result> { 454 | let cfg = oci_distribution::client::ClientConfig::default(); 455 | let mut c = oci_distribution::Client::new(cfg); 456 | 457 | let img = oci_distribution::Reference::from_str(img).map_err(|e| { 458 | crate::errors::new(crate::errors::ErrorKind::MiscHost(format!( 459 | "Failed to parse OCI distribution reference: {}", 460 | e 461 | ))) 462 | })?; 463 | let auth = if let Ok(u) = std::env::var(OCI_VAR_USER) { 464 | if let Ok(p) = std::env::var(OCI_VAR_PASSWORD) { 465 | oci_distribution::secrets::RegistryAuth::Basic(u, p) 466 | } else { 467 | oci_distribution::secrets::RegistryAuth::Anonymous 468 | } 469 | } else { 470 | oci_distribution::secrets::RegistryAuth::Anonymous 471 | }; 472 | let imgdata: Result = 473 | tokio::runtime::Runtime::new().unwrap().block_on(async { 474 | c.pull_image(&img, &auth) 475 | .await 476 | .map_err(|e| format!("{}", e).into()) 477 | }); 478 | 479 | match imgdata { 480 | Ok(imgdata) => Ok(imgdata.content), 481 | Err(e) => { 482 | error!("Failed to fetch OCI bytes: {}", e); 483 | Err(crate::errors::new(crate::errors::ErrorKind::MiscHost( 484 | "Failed to fetch OCI bytes".to_string(), 485 | ))) 486 | } 487 | } 488 | } 489 | 490 | pub(crate) fn fetch_provider_archive(img: &str) -> Result { 491 | let bytes = fetch_oci_bytes(img)?; 492 | ProviderArchive::try_load(&bytes) 493 | .map_err(|e| format!("Failed to load provider archive: {}", e).into()) 494 | } 495 | 496 | fn invocation_from_callback( 497 | hostkey: &KeyPair, 498 | origin: &str, 499 | bd: &str, 500 | ns: &str, 501 | op: &str, 502 | payload: &[u8], 503 | ) -> Invocation { 504 | let binding = if bd.trim().is_empty() { 505 | // Some actor SDKs may not specify a binding field by default 506 | "default".to_string() 507 | } else { 508 | bd.to_string() 509 | }; 510 | let target = if ns.len() == 56 && ns.starts_with("M") { 511 | WasccEntity::Actor(ns.to_string()) 512 | } else { 513 | WasccEntity::Capability { 514 | binding, 515 | capid: ns.to_string(), 516 | } 517 | }; 518 | Invocation::new( 519 | hostkey, 520 | WasccEntity::Actor(origin.to_string()), 521 | target, 522 | op, 523 | payload.to_vec(), 524 | ) 525 | } 526 | 527 | pub(crate) fn gen_config_invocation( 528 | hostkey: &KeyPair, 529 | actor: &str, 530 | capid: &str, 531 | claims: Claims, 532 | binding: String, 533 | values: HashMap, 534 | ) -> Invocation { 535 | use wascc_codec::core::*; 536 | let mut values = values.clone(); 537 | values.insert( 538 | CONFIG_WASCC_CLAIMS_ISSUER.to_string(), 539 | claims.issuer.to_string(), 540 | ); 541 | values.insert( 542 | CONFIG_WASCC_CLAIMS_CAPABILITIES.to_string(), 543 | claims 544 | .metadata 545 | .as_ref() 546 | .unwrap() 547 | .caps 548 | .as_ref() 549 | .unwrap_or(&Vec::new()) 550 | .join(","), 551 | ); 552 | values.insert(CONFIG_WASCC_CLAIMS_NAME.to_string(), claims.name()); 553 | values.insert( 554 | CONFIG_WASCC_CLAIMS_EXPIRES.to_string(), 555 | claims.expires.unwrap_or(0).to_string(), 556 | ); 557 | values.insert( 558 | CONFIG_WASCC_CLAIMS_TAGS.to_string(), 559 | claims 560 | .metadata 561 | .as_ref() 562 | .unwrap() 563 | .tags 564 | .as_ref() 565 | .unwrap_or(&Vec::new()) 566 | .join(","), 567 | ); 568 | let cfgvals = CapabilityConfiguration { 569 | module: actor.to_string(), 570 | values, 571 | }; 572 | let payload = serialize(&cfgvals).unwrap(); 573 | Invocation::new( 574 | hostkey, 575 | WasccEntity::Actor(SYSTEM_ACTOR.to_string()), 576 | WasccEntity::Capability { 577 | capid: capid.to_string(), 578 | binding, 579 | }, 580 | OP_BIND_ACTOR, 581 | payload, 582 | ) 583 | } 584 | 585 | fn sha256_digest(mut reader: R) -> Result { 586 | let mut context = Context::new(&SHA256); 587 | let mut buffer = [0; 1024]; 588 | 589 | loop { 590 | let count = reader.read(&mut buffer)?; 591 | if count == 0 { 592 | break; 593 | } 594 | context.update(&buffer[..count]); 595 | } 596 | 597 | Ok(context.finish()) 598 | } 599 | 600 | pub fn invocation_hash(target_url: &str, origin_url: &str, msg: &[u8]) -> String { 601 | use std::io::Write; 602 | let mut cleanbytes: Vec = Vec::new(); 603 | cleanbytes.write(origin_url.as_bytes()).unwrap(); 604 | cleanbytes.write(target_url.as_bytes()).unwrap(); 605 | cleanbytes.write(msg).unwrap(); 606 | let digest = sha256_digest(cleanbytes.as_slice()).unwrap(); 607 | HEXUPPER.encode(digest.as_ref()) 608 | } 609 | 610 | pub(crate) fn detect_core_host_labels() -> HashMap { 611 | let mut hm = HashMap::new(); 612 | hm.insert( 613 | CORELABEL_ARCH.to_string(), 614 | std::env::consts::ARCH.to_string(), 615 | ); 616 | hm.insert(CORELABEL_OS.to_string(), std::env::consts::OS.to_string()); 617 | hm.insert( 618 | CORELABEL_OSFAMILY.to_string(), 619 | std::env::consts::FAMILY.to_string(), 620 | ); 621 | info!("Detected Intrinsic host labels. hostcore.arch = {}, hostcore.os = {}, hostcore.family = {}", 622 | std::env::consts::ARCH, 623 | std::env::consts::OS, 624 | std::env::consts::FAMILY, 625 | ); 626 | hm 627 | } 628 | 629 | pub(crate) fn fetch_actor(actor_id: &str) -> Result { 630 | let vec = crate::inthost::fetch_oci_bytes(actor_id)?; 631 | 632 | crate::actor::Actor::from_slice(&vec) 633 | } 634 | 635 | pub(crate) fn fetch_provider( 636 | provider_ref: &str, 637 | binding_name: &str, 638 | labels: Arc>>, 639 | ) -> Result<( 640 | crate::capability::NativeCapability, 641 | Claims, 642 | )> { 643 | use std::fs::File; 644 | use std::io::Write; 645 | 646 | let par = crate::inthost::fetch_provider_archive(provider_ref)?; 647 | let lock = labels.read().unwrap(); 648 | let target = format!("{}-{}", lock[CORELABEL_ARCH], lock[CORELABEL_OS]); 649 | let v = par.target_bytes(&target); 650 | if let Some(v) = v { 651 | let path = std::env::temp_dir(); 652 | let path = path.join(target); 653 | { 654 | let mut tf = File::create(&path)?; 655 | tf.write_all(&v)?; 656 | } 657 | let nc = NativeCapability::from_file(path, Some(binding_name.to_string()))?; 658 | if let Some(c) = par.claims() { 659 | Ok((nc, c)) 660 | } else { 661 | Err(format!( 662 | "No embedded claims found in provider archive for {}", 663 | provider_ref 664 | ) 665 | .into()) 666 | } 667 | } else { 668 | Err(format!("No binary found in provider archive for {}", target).into()) 669 | } 670 | } 671 | 672 | #[cfg(test)] 673 | mod test { 674 | use super::Invocation; 675 | use crate::WasccEntity; 676 | use wascap::prelude::KeyPair; 677 | 678 | #[test] 679 | fn invocation_antiforgery() { 680 | let hostkey = KeyPair::new_server(); 681 | // As soon as we create the invocation, the claims are baked and signed with the hash embedded. 682 | let inv = Invocation::new( 683 | &hostkey, 684 | WasccEntity::Actor("testing".into()), 685 | WasccEntity::Capability { 686 | capid: "wascc:messaging".into(), 687 | binding: "default".into(), 688 | }, 689 | "OP_TESTING", 690 | vec![1, 2, 3, 4], 691 | ); 692 | let res = inv.validate_antiforgery(); 693 | println!("{:?}", res); 694 | // Obviously an invocation we just created should pass anti-forgery check 695 | assert!(inv.validate_antiforgery().is_ok()); 696 | 697 | // Let's tamper with the invocation and we should hit the hash check first 698 | let mut bad_inv = inv.clone(); 699 | bad_inv.target = WasccEntity::Actor("BADACTOR-EXFILTRATOR".into()); 700 | assert!(bad_inv.validate_antiforgery().is_err()); 701 | 702 | // Alter the payload and we should also hit the hash check 703 | let mut really_bad_inv = inv.clone(); 704 | really_bad_inv.msg = vec![5, 4, 3, 2]; 705 | assert!(really_bad_inv.validate_antiforgery().is_err()); 706 | 707 | // And just to double-check the routing address 708 | assert_eq!( 709 | inv.target_url(), 710 | "wasmbus://wascc/messaging/default/OP_TESTING" 711 | ); 712 | } 713 | } 714 | -------------------------------------------------------------------------------- /src/manifest.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | #[derive(Debug, Clone)] 4 | #[cfg_attr(feature = "manifest", derive(serde::Serialize, serde::Deserialize))] 5 | pub struct HostManifest { 6 | #[serde(default)] 7 | #[serde(skip_serializing_if = "HashMap::is_empty")] 8 | pub labels: HashMap, 9 | pub actors: Vec, 10 | pub capabilities: Vec, 11 | pub bindings: Vec, 12 | } 13 | 14 | #[derive(Debug, Clone)] 15 | #[cfg_attr(feature = "manifest", derive(serde::Serialize, serde::Deserialize))] 16 | pub struct Capability { 17 | pub path: String, 18 | pub binding_name: Option, 19 | } 20 | 21 | #[derive(Debug, Clone)] 22 | #[cfg_attr(feature = "manifest", derive(serde::Serialize, serde::Deserialize))] 23 | pub struct BindingEntry { 24 | pub actor: String, 25 | pub capability: String, 26 | pub binding: Option, 27 | pub values: Option>, 28 | } 29 | 30 | #[cfg(feature = "manifest")] 31 | use std::{fs::File, io::Read, path::Path}; 32 | #[cfg(feature = "manifest")] 33 | impl HostManifest { 34 | /// Creates an instance of a host manifest from a file path. The de-serialization 35 | /// type will be chosen based on the file path extension, selecting YAML for .yaml 36 | /// or .yml files, and JSON for all other file extensions. If the path has no extension, the 37 | /// de-serialization type chosen will be YAML. 38 | pub fn from_path( 39 | path: impl AsRef, 40 | expand_env: bool, 41 | ) -> std::result::Result> { 42 | let mut contents = String::new(); 43 | let mut file = File::open(path.as_ref())?; 44 | file.read_to_string(&mut contents)?; 45 | if expand_env { 46 | contents = Self::expand_env(&contents); 47 | } 48 | match path.as_ref().extension() { 49 | Some(e) => { 50 | let e = e.to_str().unwrap().to_lowercase(); // convert away from the FFI str 51 | if e == "yaml" || e == "yml" { 52 | serde_yaml::from_str::(&contents).map_err(|e| e.into()) 53 | } else { 54 | serde_json::from_str::(&contents).map_err(|e| e.into()) 55 | } 56 | } 57 | None => serde_yaml::from_str::(&contents).map_err(|e| e.into()), 58 | } 59 | } 60 | 61 | fn expand_env(contents: &str) -> String { 62 | let mut options = envmnt::ExpandOptions::new(); 63 | options.default_to_empty = false; // If environment variable not found, leave unexpanded. 64 | options.expansion_type = Some(envmnt::ExpansionType::UnixBracketsWithDefaults); // ${VAR:DEFAULT} 65 | 66 | envmnt::expand(contents, Some(options)) 67 | } 68 | } 69 | 70 | #[cfg(feature = "manifest")] 71 | #[cfg(test)] 72 | mod test { 73 | use super::{BindingEntry, Capability}; 74 | use std::collections::HashMap; 75 | 76 | #[test] 77 | fn round_trip() { 78 | let manifest = super::HostManifest { 79 | labels: HashMap::new(), 80 | actors: vec!["a".to_string(), "b".to_string(), "c".to_string()], 81 | capabilities: vec![ 82 | Capability { 83 | path: "one".to_string(), 84 | binding_name: Some("default".to_string()), 85 | }, 86 | Capability { 87 | path: "two".to_string(), 88 | binding_name: Some("default".to_string()), 89 | }, 90 | ], 91 | bindings: vec![BindingEntry { 92 | actor: "a".to_string(), 93 | binding: Some("default".to_string()), 94 | capability: "wascc:one".to_string(), 95 | values: Some(gen_values()), 96 | }], 97 | }; 98 | let yaml = serde_yaml::to_string(&manifest).unwrap(); 99 | assert_eq!(yaml, "---\nactors:\n - a\n - b\n - c\ncapabilities:\n - path: one\n binding_name: default\n - path: two\n binding_name: default\nbindings:\n - actor: a\n capability: \"wascc:one\"\n binding: default\n values:\n ROOT: /tmp"); 100 | } 101 | 102 | #[test] 103 | fn round_trip_with_labels() { 104 | let manifest = super::HostManifest { 105 | labels: { 106 | let mut hm = HashMap::new(); 107 | hm.insert("test".to_string(), "value".to_string()); 108 | hm 109 | }, 110 | actors: vec!["a".to_string(), "b".to_string(), "c".to_string()], 111 | capabilities: vec![ 112 | Capability { 113 | path: "one".to_string(), 114 | binding_name: Some("default".to_string()), 115 | }, 116 | Capability { 117 | path: "two".to_string(), 118 | binding_name: Some("default".to_string()), 119 | }, 120 | ], 121 | bindings: vec![BindingEntry { 122 | actor: "a".to_string(), 123 | binding: Some("default".to_string()), 124 | capability: "wascc:one".to_string(), 125 | values: Some(gen_values()), 126 | }], 127 | }; 128 | let yaml = serde_yaml::to_string(&manifest).unwrap(); 129 | assert_eq!(yaml, "---\nlabels:\n test: value\nactors:\n - a\n - b\n - c\ncapabilities:\n - path: one\n binding_name: default\n - path: two\n binding_name: default\nbindings:\n - actor: a\n capability: \"wascc:one\"\n binding: default\n values:\n ROOT: /tmp"); 130 | } 131 | 132 | #[test] 133 | fn env_expansion() { 134 | let values = vec![ 135 | "echo Test", 136 | "echo $TEST_EXPAND_ENV_TEMP", 137 | "echo ${TEST_EXPAND_ENV_TEMP}", 138 | "echo ${TEST_EXPAND_ENV_TMP}", 139 | "echo ${TEST_EXPAND_ENV_TEMP:/etc}", 140 | "echo ${TEST_EXPAND_ENV_TMP:/etc}", 141 | ]; 142 | let expected = vec![ 143 | "echo Test", 144 | "echo $TEST_EXPAND_ENV_TEMP", 145 | "echo /tmp", 146 | "echo ${TEST_EXPAND_ENV_TMP}", 147 | "echo /tmp", 148 | "echo /etc", 149 | ]; 150 | 151 | envmnt::set("TEST_EXPAND_ENV_TEMP", "/tmp"); 152 | for (got, expected) in values 153 | .iter() 154 | .map(|v| super::HostManifest::expand_env(v)) 155 | .zip(expected.iter()) 156 | { 157 | assert_eq!(*expected, got); 158 | } 159 | envmnt::remove("TEST_EXPAND_ENV_TEMP"); 160 | } 161 | 162 | fn gen_values() -> HashMap { 163 | let mut hm = HashMap::new(); 164 | hm.insert("ROOT".to_string(), "/tmp".to_string()); 165 | 166 | hm 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/middleware/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | use crate::{plugins::PluginManager, Invocation, InvocationResponse}; 3 | use std::sync::Arc; 4 | use std::sync::RwLock; 5 | use wapc::WapcHost; 6 | 7 | #[cfg(feature = "prometheus_middleware")] 8 | pub mod prometheus; 9 | 10 | /// The trait that must be implemented by all waSCC middleware 11 | pub trait Middleware: Send + Sync + 'static { 12 | fn actor_pre_invoke(&self, inv: Invocation) -> Result; 13 | fn actor_invoke( 14 | &self, 15 | inv: Invocation, 16 | handler: InvocationHandler, 17 | ) -> Result; 18 | fn actor_post_invoke(&self, response: InvocationResponse) -> Result; 19 | 20 | fn capability_pre_invoke(&self, inv: Invocation) -> Result; 21 | fn capability_invoke( 22 | &self, 23 | inv: Invocation, 24 | handler: InvocationHandler, 25 | ) -> Result; 26 | fn capability_post_invoke(&self, response: InvocationResponse) -> Result; 27 | } 28 | 29 | pub enum MiddlewareResponse { 30 | Continue(InvocationResponse), 31 | Halt(InvocationResponse), 32 | } 33 | 34 | pub struct InvocationHandler<'a> { 35 | operation: &'a dyn Fn(Invocation) -> InvocationResponse, 36 | } 37 | 38 | impl<'a> InvocationHandler<'a> { 39 | fn new(operation: &'a dyn Fn(Invocation) -> InvocationResponse) -> Self { 40 | Self { operation } 41 | } 42 | 43 | pub fn invoke(&self, inv: Invocation) -> InvocationResponse { 44 | (self.operation)(inv) 45 | } 46 | } 47 | 48 | /// Follows a chain of middleware, ultimately executing the native plugin 49 | pub(crate) fn invoke_native_capability( 50 | middlewares: Arc>>>, 51 | inv: Invocation, 52 | plugins: Arc>, 53 | ) -> Result { 54 | let inv = match run_capability_pre_invoke(inv.clone(), &middlewares.read().unwrap()) { 55 | Ok(i) => i, 56 | Err(e) => { 57 | error!("Middleware failure: {}", e); 58 | inv 59 | } 60 | }; 61 | 62 | match run_native_capability_invoke(&middlewares.read().unwrap(), &plugins.read().unwrap(), inv) 63 | { 64 | Ok(response) => { 65 | match run_capability_post_invoke(response.clone(), &middlewares.read().unwrap()) { 66 | Ok(r) => Ok(r), 67 | Err(e) => { 68 | error!("Middleware failure: {}", e); 69 | Ok(response) 70 | } 71 | } 72 | } 73 | Err(e) => Err(e), 74 | } 75 | } 76 | 77 | /// Follows a chain of middleware, ultimately executing a portable capability provider function 78 | pub(crate) fn invoke_portable_capability( 79 | middlewares: Arc>>>, 80 | inv: Invocation, 81 | guest: &WapcHost, 82 | ) -> Result { 83 | let inv = match run_capability_pre_invoke(inv.clone(), &middlewares.read().unwrap()) { 84 | Ok(i) => i, 85 | Err(e) => { 86 | error!("Middleware failure: {}", e); 87 | inv 88 | } 89 | }; 90 | 91 | match run_portable_capability_invoke(&middlewares.read().unwrap(), inv, guest) { 92 | Ok(response) => { 93 | match run_capability_post_invoke(response.clone(), &middlewares.read().unwrap()) { 94 | Ok(r) => Ok(r), 95 | Err(e) => { 96 | error!("Middleware failure: {}", e); 97 | Ok(response) 98 | } 99 | } 100 | } 101 | Err(e) => Err(e), 102 | } 103 | } 104 | 105 | pub(crate) fn invoke_actor( 106 | middlewares: Arc>>>, 107 | inv: Invocation, 108 | guest: &WapcHost, 109 | ) -> Result { 110 | let inv = match run_actor_pre_invoke(inv.clone(), &middlewares.read().unwrap()) { 111 | Ok(i) => i, 112 | Err(e) => { 113 | error!("Middleware failure: {}", e); 114 | inv 115 | } 116 | }; 117 | 118 | match run_actor_invoke(&middlewares.read().unwrap(), inv, guest) { 119 | Ok(response) => { 120 | match run_actor_post_invoke(response.clone(), &middlewares.read().unwrap()) { 121 | Ok(r) => Ok(r), 122 | Err(e) => { 123 | error!("Middleware failure: {}", e); 124 | Ok(response) 125 | } 126 | } 127 | } 128 | Err(e) => Err(e), 129 | } 130 | } 131 | 132 | fn run_actor_pre_invoke( 133 | inv: Invocation, 134 | middlewares: &[Box], 135 | ) -> Result { 136 | let mut cur_inv = inv; 137 | for m in middlewares { 138 | match m.actor_pre_invoke(cur_inv) { 139 | Ok(i) => cur_inv = i.clone(), 140 | Err(e) => return Err(e), 141 | } 142 | } 143 | Ok(cur_inv) 144 | } 145 | 146 | fn run_actor_invoke( 147 | middlewares: &[Box], 148 | inv: Invocation, 149 | guest: &WapcHost, 150 | ) -> Result { 151 | let invoke_operation = |inv: Invocation| match guest.call(&inv.operation, &inv.msg) { 152 | Ok(v) => InvocationResponse::success(&inv, v), 153 | Err(e) => InvocationResponse::error(&inv, &format!("failed to invoke actor: {}", e)), 154 | }; 155 | 156 | run_invoke(middlewares, inv, &invoke_operation) 157 | } 158 | 159 | fn run_actor_post_invoke( 160 | resp: InvocationResponse, 161 | middlewares: &[Box], 162 | ) -> Result { 163 | let mut cur_resp = resp; 164 | for m in middlewares { 165 | match m.actor_post_invoke(cur_resp) { 166 | Ok(i) => cur_resp = i.clone(), 167 | Err(e) => return Err(e), 168 | } 169 | } 170 | Ok(cur_resp) 171 | } 172 | 173 | pub(crate) fn run_capability_pre_invoke( 174 | inv: Invocation, 175 | middlewares: &[Box], 176 | ) -> Result { 177 | let mut cur_inv = inv; 178 | for m in middlewares { 179 | match m.capability_pre_invoke(cur_inv) { 180 | Ok(i) => cur_inv = i.clone(), 181 | Err(e) => return Err(e), 182 | } 183 | } 184 | Ok(cur_inv) 185 | } 186 | 187 | pub(crate) fn run_native_capability_invoke( 188 | middlewares: &[Box], 189 | plugins: &PluginManager, 190 | inv: Invocation, 191 | ) -> Result { 192 | let invoke_operation = |inv: Invocation| match plugins.call(&inv) { 193 | Ok(r) => r, 194 | Err(e) => InvocationResponse::error(&inv, &format!("failed to invoke capability: {}", e)), 195 | }; 196 | 197 | run_invoke(middlewares, inv, &invoke_operation) 198 | } 199 | 200 | pub(crate) fn run_portable_capability_invoke( 201 | middlewares: &[Box], 202 | inv: Invocation, 203 | guest: &WapcHost, 204 | ) -> Result { 205 | let invoke_operation = |inv: Invocation| match guest.call(&inv.operation, &inv.msg) { 206 | Ok(v) => InvocationResponse::success(&inv, v), 207 | Err(e) => InvocationResponse::error(&inv, &format!("failed to invoke capability: {}", e)), 208 | }; 209 | 210 | run_invoke(middlewares, inv, &invoke_operation) 211 | } 212 | 213 | fn run_invoke( 214 | middlewares: &[Box], 215 | inv: Invocation, 216 | invoke_operation: &dyn Fn(Invocation) -> InvocationResponse, 217 | ) -> Result { 218 | let mut cur_resp = Ok(InvocationResponse::error( 219 | &inv, 220 | "No middleware invoked the operation", 221 | )); 222 | 223 | for m in middlewares.iter() { 224 | match m.capability_invoke(inv.clone(), InvocationHandler::new(&invoke_operation)) { 225 | Ok(mr) => match mr { 226 | MiddlewareResponse::Continue(res) => cur_resp = Ok(res), 227 | MiddlewareResponse::Halt(res) => return Ok(res), 228 | }, 229 | Err(e) => return Err(e), 230 | } 231 | } 232 | 233 | if middlewares.is_empty() { 234 | Ok(invoke_operation(inv)) 235 | } else { 236 | cur_resp 237 | } 238 | } 239 | 240 | pub(crate) fn run_capability_post_invoke( 241 | resp: InvocationResponse, 242 | middlewares: &[Box], 243 | ) -> Result { 244 | let mut cur_resp = resp; 245 | for m in middlewares { 246 | match m.capability_post_invoke(cur_resp) { 247 | Ok(i) => cur_resp = i.clone(), 248 | Err(e) => return Err(e), 249 | } 250 | } 251 | Ok(cur_resp) 252 | } 253 | 254 | #[cfg(test)] 255 | mod tests { 256 | use std::sync::atomic::{AtomicUsize, Ordering}; 257 | 258 | use super::Middleware; 259 | use crate::inthost::{Invocation, InvocationResponse, WasccEntity}; 260 | use crate::middleware::{InvocationHandler, MiddlewareResponse}; 261 | use crate::Result; 262 | use wascap::prelude::KeyPair; 263 | 264 | struct IncMiddleware { 265 | pre: &'static AtomicUsize, 266 | post: &'static AtomicUsize, 267 | cap_pre: &'static AtomicUsize, 268 | cap_post: &'static AtomicUsize, 269 | } 270 | 271 | impl Middleware for IncMiddleware { 272 | fn actor_pre_invoke(&self, inv: Invocation) -> Result { 273 | self.pre.fetch_add(1, Ordering::SeqCst); 274 | Ok(inv) 275 | } 276 | fn actor_invoke( 277 | &self, 278 | inv: Invocation, 279 | handler: InvocationHandler, 280 | ) -> Result { 281 | Ok(MiddlewareResponse::Continue(handler.invoke(inv))) 282 | } 283 | fn actor_post_invoke(&self, response: InvocationResponse) -> Result { 284 | self.post.fetch_add(1, Ordering::SeqCst); 285 | Ok(response) 286 | } 287 | fn capability_pre_invoke(&self, inv: Invocation) -> Result { 288 | self.cap_pre.fetch_add(1, Ordering::SeqCst); 289 | Ok(inv) 290 | } 291 | fn capability_invoke( 292 | &self, 293 | inv: Invocation, 294 | handler: InvocationHandler, 295 | ) -> Result { 296 | Ok(MiddlewareResponse::Continue(handler.invoke(inv))) 297 | } 298 | fn capability_post_invoke( 299 | &self, 300 | response: InvocationResponse, 301 | ) -> Result { 302 | self.cap_post.fetch_add(1, Ordering::SeqCst); 303 | Ok(response) 304 | } 305 | } 306 | 307 | static PRE: AtomicUsize = AtomicUsize::new(0); 308 | static POST: AtomicUsize = AtomicUsize::new(0); 309 | static CAP_PRE: AtomicUsize = AtomicUsize::new(0); 310 | static CAP_POST: AtomicUsize = AtomicUsize::new(0); 311 | 312 | #[test] 313 | fn simple_add() { 314 | let inc_mid = IncMiddleware { 315 | pre: &PRE, 316 | post: &POST, 317 | cap_pre: &CAP_PRE, 318 | cap_post: &CAP_POST, 319 | }; 320 | let hk = KeyPair::new_server(); 321 | 322 | let mids: Vec> = vec![Box::new(inc_mid)]; 323 | let inv = Invocation::new( 324 | &hk, 325 | WasccEntity::Actor("test".to_string()), 326 | WasccEntity::Capability { 327 | capid: "testing:sample".to_string(), 328 | binding: "default".to_string(), 329 | }, 330 | "testing", 331 | b"abc1234".to_vec(), 332 | ); 333 | let res = super::run_actor_pre_invoke(inv.clone(), &mids); 334 | assert!(res.is_ok()); 335 | let res2 = super::run_actor_pre_invoke(inv, &mids); 336 | assert!(res2.is_ok()); 337 | assert_eq!(PRE.fetch_add(0, Ordering::SeqCst), 2); 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /src/plugins.rs: -------------------------------------------------------------------------------- 1 | use crate::capability::NativeCapability; 2 | use crate::dispatch::WasccNativeDispatcher; 3 | use crate::errors::{self, ErrorKind}; 4 | use crate::inthost::Invocation; 5 | use crate::inthost::{InvocationResponse, WasccEntity}; 6 | use crate::{Result, RouteKey}; 7 | use std::collections::HashMap; 8 | 9 | #[derive(Default)] 10 | pub(crate) struct PluginManager { 11 | plugins: HashMap, 12 | } 13 | 14 | impl PluginManager { 15 | pub fn register_dispatcher( 16 | &mut self, 17 | binding: &str, 18 | capid: &str, 19 | dispatcher: WasccNativeDispatcher, 20 | ) -> Result<()> { 21 | let key = RouteKey::new(binding, capid); 22 | match self.plugins.get(&key) { 23 | Some(p) => match p.plugin.configure_dispatch(Box::new(dispatcher)) { 24 | Ok(_) => Ok(()), 25 | Err(_) => Err(errors::new(ErrorKind::CapabilityProvider( 26 | "Failed to configure dispatch on provider".into(), 27 | ))), 28 | }, 29 | None => Err(errors::new(ErrorKind::CapabilityProvider( 30 | "Attempt to register dispatcher for non-existent plugin".into(), 31 | ))), 32 | } 33 | } 34 | 35 | pub fn call(&self, inv: &Invocation) -> Result { 36 | if let WasccEntity::Capability { capid, binding } = &inv.target { 37 | let route_key = RouteKey::new(&binding, &capid); 38 | let actor = if let WasccEntity::Actor(s) = &inv.origin { 39 | s.to_string() 40 | } else { 41 | "SHOULD NEVER SEND CAP-ORIGIN INVOCATION TO ANOTHER CAP".to_string() 42 | }; 43 | match self.plugins.get(&route_key) { 44 | // native capability is registered via plugin 45 | Some(c) => match c.plugin.handle_call(&actor, &inv.operation, &inv.msg) { 46 | Ok(msg) => Ok(InvocationResponse::success(inv, msg)), 47 | Err(e) => Err(errors::new(errors::ErrorKind::HostCallFailure(e))), 48 | }, 49 | // if there's no plugin, return an error 50 | None => Err(errors::new(ErrorKind::CapabilityProvider(format!( 51 | "No such capability ID registered as native plug-in {:?}", 52 | route_key 53 | )))), 54 | } 55 | } else { 56 | Err(errors::new(ErrorKind::MiscHost( 57 | "Attempted to invoke a capability provider plugin as though it were an actor. Bad route?".into() 58 | ))) 59 | } 60 | } 61 | 62 | pub fn add_plugin(&mut self, plugin: NativeCapability) -> Result<()> { 63 | let key = RouteKey::new(&plugin.binding_name, &plugin.id()); 64 | if self.plugins.contains_key(&key) { 65 | Err(errors::new(errors::ErrorKind::CapabilityProvider(format!( 66 | "Duplicate capability ID attempted to register provider: ({},{})", 67 | plugin.binding_name, 68 | plugin.id() 69 | )))) 70 | } else { 71 | self.plugins.insert(key, plugin); 72 | Ok(()) 73 | } 74 | } 75 | 76 | pub fn remove_plugin(&mut self, binding: &str, capid: &str) -> Result<()> { 77 | let key = RouteKey::new(&binding, &capid); 78 | if let Some(plugin) = self.plugins.remove(&key) { 79 | drop(plugin); 80 | } 81 | Ok(()) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/spawns.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | 3 | use crate::inthost::*; 4 | use crate::BindingsList; 5 | use crate::{ 6 | bus::MessageBus, dispatch::WasccNativeDispatcher, plugins::PluginManager, Authorizer, 7 | Invocation, InvocationResponse, Middleware, RouteKey, 8 | }; 9 | use crate::{middleware, NativeCapability}; 10 | 11 | use crossbeam::{Receiver, Sender}; 12 | use crossbeam_channel as channel; 13 | use crossbeam_utils::sync::WaitGroup; 14 | #[cfg(feature = "lattice")] 15 | use latticeclient::BusEvent; 16 | use std::collections::HashMap; 17 | use std::sync::{Arc, RwLock}; 18 | use std::thread; 19 | use wapc::{WapcHost, WasiParams}; 20 | use wascap::{jwt::Claims, prelude::KeyPair}; 21 | use wascc_codec::{ 22 | capabilities::{CapabilityDescriptor, OP_GET_CAPABILITY_DESCRIPTOR}, 23 | core::{CapabilityConfiguration, OP_BIND_ACTOR, OP_REMOVE_ACTOR}, 24 | deserialize, serialize, SYSTEM_ACTOR, 25 | }; 26 | 27 | /// Spawns a new background thread in which a new `WapcHost` is created for the actor 28 | /// module bytes. A message bus subscription is created either for the actor's RPC 29 | /// subject OR for the capability provider's root subject. We then select between a receive 30 | /// invocation on the subscription's channel or a receive invocation on the terminator channel, 31 | /// which will then trigger a cleanup of the actor's resources. 32 | pub(crate) fn spawn_actor( 33 | wg: WaitGroup, 34 | claims: Claims, 35 | buf: Vec, 36 | wasi: Option, 37 | actor: bool, 38 | binding: Option, 39 | bus: Arc, 40 | mids: Arc>>>, 41 | caps: Arc>>, 42 | bindings: Arc>, 43 | claimsmap: Arc>>>, 44 | terminators: Arc>>>, 45 | hk: KeyPair, 46 | auth: Arc>>, 47 | image_map: Arc>>, 48 | imgref: Option, 49 | ) -> Result<()> { 50 | let c = claims.clone(); 51 | let b = bus.clone(); 52 | let seed = hk.seed().unwrap(); 53 | let s = seed.clone(); 54 | let hostkey = KeyPair::from_seed(&hk.seed().unwrap()).unwrap(); 55 | let authorizer = auth.clone(); 56 | 57 | thread::spawn(move || { 58 | let hk = KeyPair::from_seed(&seed).unwrap(); 59 | if actor { 60 | #[cfg(feature = "lattice")] 61 | let _ = bus.publish_event(BusEvent::ActorStarting { 62 | host: hostkey.public_key(), 63 | actor: claims.subject.to_string(), 64 | }); 65 | } 66 | #[cfg(feature = "wasmtime")] 67 | let engine = wasmtime_provider::WasmtimeEngineProvider::new(&buf, wasi); 68 | #[cfg(feature = "wasm3")] 69 | let engine = wasm3_provider::Wasm3EngineProvider::new(&buf); 70 | 71 | let mut guest = WapcHost::new(Box::new(engine), move |_id, bd, ns, op, payload| { 72 | let key = KeyPair::from_seed(&s).unwrap(); 73 | wapc_host_callback( 74 | key, 75 | c.clone(), 76 | bus.clone(), 77 | bd, 78 | ns, 79 | op, 80 | payload, 81 | authorizer.clone(), 82 | ) 83 | }) 84 | .unwrap(); 85 | let mut d: Option = None; 86 | 87 | let subscribe_subject = if actor { 88 | b.actor_subject(&claims.subject) 89 | } else { 90 | d = match get_descriptor(&mut guest) { 91 | Ok(d) => Some(d), 92 | Err(_) => None, 93 | }; 94 | if d.is_none() { 95 | return "".to_string(); 96 | } 97 | let capid = d.as_ref().unwrap().id.to_string(); 98 | let bname = binding.clone().unwrap(); 99 | #[cfg(feature = "lattice")] 100 | let _ = b.publish_event(BusEvent::ProviderLoaded { 101 | host: hostkey.public_key(), 102 | capid: capid.to_string(), 103 | instance_name: bname.to_string(), 104 | }); 105 | 106 | b.provider_subject(&capid, &bname) 107 | }; 108 | 109 | let (inv_s, inv_r): (Sender, Receiver) = channel::unbounded(); 110 | let (resp_s, resp_r): (Sender, Receiver) = 111 | channel::unbounded(); 112 | let (term_s, term_r): (Sender, Receiver) = channel::unbounded(); 113 | 114 | if subscribe_subject.is_empty() { 115 | return "can't subscribe to message bus".to_string(); 116 | } 117 | terminators 118 | .write() 119 | .unwrap() 120 | .insert(subscribe_subject.clone(), term_s); 121 | let _ = b.subscribe(&subscribe_subject, inv_s, resp_r).unwrap(); 122 | drop(wg); // Let the Host wrapper function return 123 | if actor { 124 | #[cfg(feature = "lattice")] 125 | let _ = b.publish_event(BusEvent::ActorStarted { 126 | host: hostkey.public_key(), 127 | actor: claims.subject.to_string(), 128 | }); 129 | info!("Actor {} up and running.", &claims.subject); 130 | } 131 | loop { 132 | let key = KeyPair::from_seed(&seed).unwrap(); 133 | select! { 134 | recv(inv_r) -> inv => { 135 | if let Ok(inv) = inv { 136 | let inv_r = if actor { 137 | middleware::invoke_actor(mids.clone(), inv.clone(), &mut guest).unwrap() 138 | } else { 139 | if inv.operation != OP_BIND_ACTOR && inv.operation != OP_GET_CAPABILITY_DESCRIPTOR { 140 | InvocationResponse::error(&inv, "Attempted to invoke binding-required operation on unbound provider") 141 | } else { 142 | middleware::invoke_portable_capability(mids.clone(), inv.clone(), &mut guest).unwrap() 143 | } 144 | }; 145 | resp_s.send(inv_r.clone()).unwrap(); 146 | if inv.operation == OP_BIND_ACTOR && !actor && inv_r.error.is_none() { 147 | spawn_bound_portable_capability(); 148 | } 149 | } 150 | }, 151 | recv(term_r) -> _term => { 152 | info!("Terminating {} {}", if actor { "actor" } else { "capability" }, &claims.subject); 153 | let _ = b.unsubscribe(&subscribe_subject); 154 | terminators.write().unwrap().remove(&subscribe_subject); 155 | if !actor { 156 | //#[cfg(feature = "lattice")] 157 | //let _ = bus.publish_event(BusEvent::ProviderRemoved{ host: hostkey.public_key(), actor: claims.subject.to_string() }); 158 | remove_cap(caps.clone(), &d.as_ref().unwrap().id, binding.as_ref().unwrap()); // for cap providers, route key is the capid 159 | unbind_all_from_cap(bindings.clone(), &d.unwrap().id, binding.as_ref().unwrap()); 160 | } else { 161 | #[cfg(feature = "lattice")] 162 | let _ = b.publish_event(BusEvent::ActorStopped{ host: hostkey.public_key(), actor: claims.subject.to_string() }); 163 | 164 | let mut lock = claimsmap.write().unwrap(); 165 | let _ = lock.remove(&claims.subject); 166 | drop(lock); 167 | if let Some(ref ir) = imgref { // if this actor was added via OCI image ref, remove the mapping 168 | let mut lock = image_map.write().unwrap(); 169 | let _ = lock.remove(ir); 170 | drop(lock); 171 | } 172 | deconfigure_actor(key,b.clone(), bindings.clone(), &claims.subject); 173 | } 174 | break "".to_string(); // TODO: WHY WHY WHY does this recv arm need to return a value?!?!? 175 | } 176 | } 177 | } 178 | }); 179 | 180 | Ok(()) 181 | } 182 | 183 | pub(crate) fn spawn_native_capability( 184 | capability: NativeCapability, 185 | bus: Arc, 186 | mids: Arc>>>, 187 | bindings: Arc>, 188 | terminators: Arc>>>, 189 | plugins: Arc>, 190 | wg: WaitGroup, 191 | hk: Arc, 192 | ) -> Result<()> { 193 | let capid = capability.id().to_string(); 194 | let binding = capability.binding_name.to_string(); 195 | let b = bus.clone(); 196 | 197 | let b2 = bus.clone(); 198 | let mid2 = mids.clone(); 199 | let binding2 = bindings.clone(); 200 | let plugin2 = plugins.clone(); 201 | let h2 = hk.clone(); 202 | let t2 = terminators.clone(); 203 | let capid2 = capid.clone(); 204 | let bindingname2 = binding.clone(); 205 | 206 | plugins.write().unwrap().add_plugin(capability)?; 207 | 208 | thread::spawn(move || { 209 | let (inv_s, inv_r): (Sender, Receiver) = channel::unbounded(); 210 | let (resp_s, resp_r): (Sender, Receiver) = 211 | channel::unbounded(); 212 | let (term_s, term_r): (Sender, Receiver) = channel::unbounded(); 213 | let subscribe_subject = bus.provider_subject(&capid, &binding); 214 | 215 | let _ = bus.nqsubscribe(&subscribe_subject, inv_s, resp_r).unwrap(); 216 | let dispatcher = WasccNativeDispatcher::new(hk.clone(), bus.clone(), &capid, &binding); 217 | plugins 218 | .write() 219 | .unwrap() 220 | .register_dispatcher(&binding, &capid, dispatcher) 221 | .unwrap(); 222 | terminators 223 | .write() 224 | .unwrap() 225 | .insert(subscribe_subject.to_string(), term_s); 226 | 227 | info!("Native capability provider '({},{})' ready", binding, capid); 228 | 229 | drop(wg); 230 | #[cfg(feature = "lattice")] 231 | let _ = b.publish_event(BusEvent::ProviderLoaded { 232 | host: hk.public_key(), 233 | capid: capid.to_string(), 234 | instance_name: binding.to_string(), 235 | }); 236 | 237 | loop { 238 | select! { 239 | recv(inv_r) -> inv => { 240 | if let Ok(inv) = inv { 241 | let inv_r = if inv.operation != OP_BIND_ACTOR && inv.operation != OP_GET_CAPABILITY_DESCRIPTOR && inv.operation != OP_REMOVE_ACTOR { 242 | InvocationResponse::error(&inv, "Attempted to invoke binding-required operation on unbound provider") 243 | } else { 244 | middleware::invoke_native_capability(mids.clone(), inv.clone(), plugins.clone()).unwrap() 245 | }; 246 | resp_s.send(inv_r.clone()).unwrap(); 247 | if inv.operation == OP_BIND_ACTOR && inv_r.error.is_none() { 248 | spawn_bound_native_capability(bus.clone(), inv.clone(), &capid, &binding, mids.clone(), plugins.clone(), terminators.clone(), bindings.clone(), hk.clone()); 249 | } 250 | if inv.operation == OP_REMOVE_ACTOR && inv_r.error.is_none() { 251 | let actor = actor_from_config(&inv.msg); 252 | let key = bus.provider_subject_bound_actor(&capid, &binding, &actor); 253 | terminators.read().unwrap()[&key].send(true).unwrap(); 254 | } 255 | } 256 | }, 257 | recv(term_r) -> _term => { 258 | info!("Terminating native capability provider {},{}", binding, capid); 259 | unbind_all_from_cap(bindings.clone(), &capid, &binding); 260 | unsub_all_bindings(bindings.clone(), bus.clone(), &capid); 261 | let _ = bus.unsubscribe(&subscribe_subject); 262 | plugins.write().unwrap().remove_plugin(&binding, &capid).unwrap(); 263 | terminators.write().unwrap().remove(&subscribe_subject); 264 | #[cfg(feature="lattice")] 265 | let _ = b.publish_event(BusEvent::ProviderRemoved{ host: hk.public_key(), capid: capid.to_string(), instance_name: binding.to_string()}); 266 | break; 267 | } 268 | } 269 | } 270 | }); 271 | 272 | #[cfg(feature = "lattice")] 273 | reestablish_bindings( 274 | b2.clone(), 275 | mid2.clone(), 276 | binding2.clone(), 277 | plugin2.clone(), 278 | t2.clone(), 279 | h2.clone(), 280 | &capid2, 281 | &bindingname2, 282 | ); 283 | Ok(()) 284 | } 285 | 286 | #[cfg(feature = "lattice")] 287 | fn reestablish_bindings( 288 | bus: Arc, 289 | mids: Arc>>>, 290 | bindings: Arc>, 291 | plugins: Arc>, 292 | terminators: Arc>>>, 293 | hk: Arc, 294 | capid: &str, 295 | binding_name: &str, 296 | ) { 297 | // 1. load pre-existing bindings from bus 298 | // 2. for each binding, invoke OP_BIND_ACTOR on the root capability 299 | // 3. if successful, spawn the bound actor-capability comms thread 300 | if let Ok(blist) = bus.query_bindings() { 301 | for b in blist { 302 | if b.capability_id == capid && b.binding_name == binding_name { 303 | let cfgvals = CapabilityConfiguration { 304 | module: b.actor.to_string(), 305 | values: b.configuration.clone(), 306 | }; 307 | let payload = serialize(&cfgvals).unwrap(); 308 | let inv = Invocation::new( 309 | &hk, 310 | WasccEntity::Actor(SYSTEM_ACTOR.to_string()), 311 | WasccEntity::Capability { 312 | capid: capid.to_string(), 313 | binding: binding_name.to_string(), 314 | }, 315 | OP_BIND_ACTOR, 316 | payload, 317 | ); 318 | let inv_r = middleware::invoke_native_capability( 319 | mids.clone(), 320 | inv.clone(), 321 | plugins.clone(), 322 | ) 323 | .unwrap(); 324 | if inv_r.error.is_none() { 325 | info!( 326 | "Re-establishing binding between {} and {},{}", 327 | &b.actor, &capid, &binding_name 328 | ); 329 | spawn_bound_native_capability( 330 | bus.clone(), 331 | inv.clone(), 332 | &capid, 333 | &binding_name, 334 | mids.clone(), 335 | plugins.clone(), 336 | terminators.clone(), 337 | bindings.clone(), 338 | hk.clone(), 339 | ); 340 | } 341 | } 342 | } 343 | } 344 | } 345 | 346 | fn actor_from_config(bytes: &[u8]) -> String { 347 | let config: CapabilityConfiguration = deserialize(bytes).unwrap(); 348 | config.module 349 | } 350 | 351 | // This is a thread that handles the private conversations between an actor and a capability. 352 | // On the lattice, this means that actor-to-provider requests occur on a topic made up of actor+provider capid+provider instance/binding name 353 | fn spawn_bound_native_capability( 354 | bus: Arc, 355 | inv: Invocation, 356 | capid: &str, 357 | binding: &str, 358 | middlewares: Arc>>>, 359 | plugins: Arc>, 360 | terminators: Arc>>>, 361 | bindings: Arc>, 362 | hk: Arc, 363 | ) { 364 | let capid = capid.to_string(); 365 | let binding = binding.to_string(); 366 | 367 | let config: CapabilityConfiguration = deserialize(&inv.msg).unwrap(); 368 | let actor = config.module.to_string(); 369 | let mids = middlewares.clone(); 370 | let terms = terminators.clone(); 371 | 372 | thread::spawn(move || { 373 | let (inv_s, inv_r): (Sender, Receiver) = channel::unbounded(); 374 | let (resp_s, resp_r): (Sender, Receiver) = 375 | channel::unbounded(); 376 | let subscribe_subject = bus.provider_subject_bound_actor(&capid, &binding, &actor); 377 | let (term_s, term_r): (Sender, Receiver) = channel::unbounded(); 378 | 379 | let _ = bus.subscribe(&subscribe_subject, inv_s, resp_r).unwrap(); 380 | terms 381 | .write() 382 | .unwrap() 383 | .insert(subscribe_subject.to_string(), term_s.clone()); 384 | 385 | loop { 386 | select! { 387 | recv(inv_r) -> inv => { 388 | if let Ok(inv) = inv { 389 | let inv_r = middleware::invoke_native_capability(mids.clone(), inv.clone(), plugins.clone()).unwrap(); 390 | resp_s.send(inv_r).unwrap(); 391 | } 392 | }, 393 | recv(term_r) -> _term => { 394 | let _ = bus.unsubscribe(&subscribe_subject); 395 | remove_binding(bindings.clone(), &actor, &binding, &capid); 396 | terminators.write().unwrap().remove(&subscribe_subject); 397 | #[cfg(feature="lattice")] 398 | let _ = bus.publish_event(BusEvent::ProviderRemoved{ host: hk.public_key(), capid: capid.to_string(), instance_name: binding.to_string()}); 399 | break; 400 | } 401 | } 402 | } 403 | }); 404 | } 405 | 406 | fn spawn_bound_portable_capability() { 407 | todo!() 408 | } 409 | -------------------------------------------------------------------------------- /tests/auth.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use wascc_host::{Actor, Authorizer, Host, HostBuilder, NativeCapability}; 3 | 4 | pub(crate) fn default_authorizer_enforces_cap_attestations() -> Result<(), Box> { 5 | // Attempt to bind an actor to a capability for which it isn't authorized. 6 | #[cfg(feature = "lattice")] 7 | let host = HostBuilder::new() 8 | .with_lattice_namespace("authorizercaps") 9 | .build(); 10 | 11 | #[cfg(not(feature = "lattice"))] 12 | let host = HostBuilder::new().build(); 13 | 14 | host.add_actor(Actor::from_file("./examples/.assets/kvcounter.wasm")?)?; 15 | 16 | host.add_native_capability(NativeCapability::from_file( 17 | "./examples/.assets/libwascc_nats.so", 18 | None, 19 | )?)?; 20 | 21 | let res = host.set_binding( 22 | "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ", 23 | "wascc:messaging", 24 | None, 25 | crate::common::empty_config(), 26 | ); 27 | assert_eq!(res.err().unwrap().to_string(), "WebAssembly module authorization failure: Unauthorized binding: actor MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ is not authorized to use capability wascc:messaging."); 28 | host.shutdown()?; 29 | std::thread::sleep(::std::time::Duration::from_millis(500)); 30 | Ok(()) 31 | } 32 | 33 | pub(crate) fn authorizer_blocks_bindings() -> Result<(), Box> { 34 | // Set the authorizer before calling a bind_actor, and bind_actor should 35 | // return permission denied / Err if the authorizer denies that invocation. 36 | #[cfg(not(feature = "lattice"))] 37 | let host = HostBuilder::new() 38 | .with_authorizer(DenyAuthorizer::new(false, true)) 39 | .build(); 40 | 41 | #[cfg(feature = "lattice")] 42 | let host = HostBuilder::new() 43 | .with_lattice_namespace("authorizerblocks") 44 | .with_authorizer(DenyAuthorizer::new(false, true)) 45 | .build(); 46 | 47 | host.add_actor(Actor::from_file("./examples/.assets/kvcounter.wasm")?)?; 48 | host.add_native_capability(NativeCapability::from_file( 49 | "./examples/.assets/libwascc_httpsrv.so", 50 | None, 51 | )?)?; 52 | host.add_native_capability(NativeCapability::from_file( 53 | "./examples/.assets/libwascc_redis.so", 54 | None, 55 | )?)?; 56 | 57 | let res = host.set_binding( 58 | "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ", 59 | "wascc:keyvalue", 60 | None, 61 | crate::common::redis_config(), 62 | ); 63 | 64 | assert!(res.is_err()); 65 | host.shutdown()?; 66 | std::thread::sleep(::std::time::Duration::from_millis(500)); 67 | Ok(()) 68 | } 69 | 70 | pub(crate) fn authorizer_blocks_load() -> Result<(), Box> { 71 | // Set the authorizer before calling a bind_actor, and bind_actor should 72 | // return permission denied / Err if the authorizer denies that invocation. 73 | let host = HostBuilder::new() 74 | .with_authorizer(DenyAuthorizer::new(true, false)) 75 | .build(); 76 | 77 | let res = host.add_actor(Actor::from_file("./examples/.assets/kvcounter.wasm")?); 78 | 79 | assert!(res.is_err()); 80 | host.shutdown()?; 81 | std::thread::sleep(::std::time::Duration::from_millis(700)); 82 | Ok(()) 83 | } 84 | 85 | struct DenyAuthorizer { 86 | deny_load: bool, 87 | deny_invoke: bool, 88 | } 89 | 90 | impl DenyAuthorizer { 91 | pub(crate) fn new(deny_load: bool, deny_invoke: bool) -> DenyAuthorizer { 92 | DenyAuthorizer { 93 | deny_load, 94 | deny_invoke, 95 | } 96 | } 97 | } 98 | impl Authorizer for DenyAuthorizer { 99 | fn can_load(&self, _claims: &wascap::prelude::Claims) -> bool { 100 | !self.deny_load 101 | } 102 | fn can_invoke( 103 | &self, 104 | _claims: &wascap::prelude::Claims, 105 | _target: &wascc_host::WasccEntity, 106 | _operation: &str, 107 | ) -> bool { 108 | !self.deny_invoke 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tests/common.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Write}; 2 | use std::{collections::HashMap, error::Error}; 3 | use wascc_host::{Actor, Host, NativeCapability}; 4 | 5 | pub fn get_hello_actor() -> Result> { 6 | Actor::from_file("./examples/.assets/echo.wasm").map_err(|e| e.into()) 7 | } 8 | 9 | pub fn get_hello2_actor() -> Result> { 10 | Actor::from_file("./examples/.assets/echo2.wasm").map_err(|e| e.into()) 11 | } 12 | 13 | pub fn gen_stock_host(first_port: u16) -> Result> { 14 | let host = Host::new(); 15 | host.add_actor(get_hello_actor()?)?; 16 | host.add_actor(get_hello2_actor()?)?; 17 | host.add_native_capability(NativeCapability::from_file( 18 | "./examples/.assets/libwascc_httpsrv.so", 19 | Some("stockhost".to_string()), 20 | )?)?; 21 | 22 | host.set_binding( 23 | "MDFD7XZ5KBOPLPHQKHJEMPR54XIW6RAG5D7NNKN22NP7NSEWNTJZP7JN", 24 | "wascc:http_server", 25 | Some("stockhost".to_string()), 26 | generate_port_config(first_port), 27 | )?; 28 | 29 | host.set_binding( 30 | "MB4OLDIC3TCZ4Q4TGGOVAZC43VXFE2JQVRAXQMQFXUCREOOFEKOKZTY2", 31 | "wascc:http_server", 32 | Some("stockhost".to_string()), 33 | generate_port_config(first_port + 1), 34 | )?; 35 | 36 | Ok(host) 37 | } 38 | 39 | pub fn gen_kvcounter_host(port: u16, host: Host) -> Result> { 40 | host.add_actor(Actor::from_file("./examples/.assets/kvcounter.wasm")?)?; 41 | host.add_native_capability(NativeCapability::from_file( 42 | "./examples/.assets/libwascc_httpsrv.so", 43 | None, 44 | )?)?; 45 | host.add_native_capability(NativeCapability::from_file( 46 | "./examples/.assets/libwascc_redis.so", 47 | None, 48 | )?)?; 49 | 50 | host.set_binding( 51 | "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ", 52 | "wascc:keyvalue", 53 | None, 54 | redis_config(), 55 | )?; 56 | host.set_binding( 57 | "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ", 58 | "wascc:http_server", 59 | None, 60 | generate_port_config(port), 61 | )?; 62 | 63 | Ok(host) 64 | } 65 | 66 | pub fn redis_config() -> HashMap { 67 | let mut hm = HashMap::new(); 68 | hm.insert("URL".to_string(), "redis://127.0.0.1:6379".to_string()); 69 | 70 | hm 71 | } 72 | 73 | pub fn empty_config() -> HashMap { 74 | HashMap::new() 75 | } 76 | 77 | pub fn generate_port_config(port: u16) -> HashMap { 78 | let mut hm = HashMap::new(); 79 | hm.insert("PORT".to_string(), port.to_string()); 80 | 81 | hm 82 | } 83 | 84 | pub fn generate_resigned_actor(bytes: &[u8]) -> Result> { 85 | use wascap::prelude::*; 86 | 87 | let (issuer, module) = (KeyPair::new_account(), KeyPair::new_module()); 88 | let claims = ClaimsBuilder::::new() 89 | .issuer(&issuer.public_key()) 90 | .subject(&module.public_key()) 91 | .with_metadata(Actor { 92 | name: Some("test".to_string()), 93 | caps: Some(vec![ 94 | caps::HTTP_SERVER.to_string(), 95 | caps::KEY_VALUE.to_string(), 96 | ]), 97 | ..Default::default() 98 | }) 99 | .build(); 100 | let embedded = wasm::embed_claims(&bytes, &claims, &issuer)?; 101 | 102 | Ok(wascc_host::Actor::from_slice(&embedded)?) 103 | } 104 | -------------------------------------------------------------------------------- /tests/core.rs: -------------------------------------------------------------------------------- 1 | use reqwest; 2 | use std::error::Error; 3 | use wascc_host::Host; 4 | 5 | pub(crate) fn stock_host() -> Result<(), Box> { 6 | let host = crate::common::gen_stock_host(9090)?; 7 | assert_eq!(2, host.actors().len()); 8 | if let Some(ref claims) = 9 | host.claims_for_actor("MB4OLDIC3TCZ4Q4TGGOVAZC43VXFE2JQVRAXQMQFXUCREOOFEKOKZTY2") 10 | { 11 | let md = claims.metadata.as_ref().unwrap(); 12 | assert!(md 13 | .caps 14 | .as_ref() 15 | .unwrap() 16 | .contains(&"wascc:http_server".to_string())); 17 | } 18 | 19 | std::thread::sleep(::std::time::Duration::from_millis(500)); 20 | 21 | let resp = reqwest::blocking::get("http://localhost:9090")?; 22 | assert!(resp.status().is_success()); 23 | assert_eq!(resp.text()?, 24 | "{\"method\":\"GET\",\"path\":\"/\",\"query_string\":\"\",\"headers\":{\"accept\":\"*/*\",\"host\":\"localhost:9090\"},\"body\":[]}" 25 | ); 26 | host.shutdown()?; 27 | std::thread::sleep(::std::time::Duration::from_millis(500)); 28 | Ok(()) 29 | } 30 | 31 | pub(crate) fn kv_host() -> Result<(), Box> { 32 | use redis::Commands; 33 | 34 | let host = crate::common::gen_kvcounter_host(8083, Host::new())?; 35 | std::thread::sleep(::std::time::Duration::from_millis(100)); 36 | let key = uuid::Uuid::new_v4().to_string(); 37 | let rkey = format!(":{}", key); // the kv wasm logic does a replace on '/' with ':' 38 | let url = format!("http://localhost:8083/{}", key); 39 | let client = redis::Client::open("redis://127.0.0.1/")?; 40 | let mut con = client.get_connection()?; 41 | 42 | let mut resp = reqwest::blocking::get(&url)?; 43 | assert!(resp.status().is_success()); 44 | reqwest::blocking::get(&url)?; 45 | resp = reqwest::blocking::get(&url)?; // counter should be at 3 now 46 | assert!(resp.status().is_success()); 47 | assert_eq!(resp.text()?, "{\"counter\":3}"); 48 | host.shutdown()?; 49 | 50 | let _: () = con.del(&rkey)?; 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /tests/lattice.rs: -------------------------------------------------------------------------------- 1 | use latticeclient::Client; 2 | use std::error::Error; 3 | 4 | pub(crate) fn lattice_single_host() -> Result<(), Box> { 5 | use std::time::Duration; 6 | use wascc_host::{Host, HostBuilder}; 7 | 8 | let host = HostBuilder::new() 9 | .with_label("integration", "test") 10 | .with_label("hostcore.arch", "FOOBAR") 11 | .with_lattice_namespace("singlehost") 12 | .build(); 13 | 14 | let delay = Duration::from_millis(500); 15 | std::thread::sleep(delay); 16 | 17 | let lc = Client::new("127.0.0.1", None, delay, Some("singlehost".to_string())); 18 | let hosts = lc.get_hosts()?; 19 | assert_eq!(hosts.len(), 1); 20 | assert_eq!(hosts[0].labels["hostcore.os"], std::env::consts::OS); 21 | assert_eq!( 22 | hosts[0].labels["hostcore.osfamily"], 23 | std::env::consts::FAMILY 24 | ); 25 | assert_eq!(hosts[0].labels["hostcore.arch"], std::env::consts::ARCH); 26 | assert_eq!(hosts[0].labels["integration"], "test"); 27 | host.shutdown()?; 28 | std::thread::sleep(delay); 29 | Ok(()) 30 | } 31 | 32 | pub(crate) fn lattice_isolation() -> Result<(), Box> { 33 | use std::time::Duration; 34 | use wascc_host::{Host, HostBuilder}; 35 | let host1 = HostBuilder::new() 36 | .with_lattice_namespace("system1") 37 | .with_label("testval", "1") 38 | .build(); 39 | let host2 = HostBuilder::new() 40 | .with_lattice_namespace("system2") 41 | .with_label("testval", "2") 42 | .build(); 43 | 44 | let delay = Duration::from_millis(500); 45 | std::thread::sleep(delay); 46 | 47 | let lc1 = Client::new("127.0.0.1", None, delay, Some("system1".to_string())); 48 | let hosts1 = lc1.get_hosts()?; 49 | assert_eq!(hosts1.len(), 1); 50 | assert_eq!(hosts1[0].labels["testval"], "1"); 51 | 52 | let lc2 = Client::new("127.0.0.1", None, delay, Some("system2".to_string())); 53 | let hosts2 = lc2.get_hosts()?; 54 | assert_eq!(hosts2.len(), 1); 55 | assert_eq!(hosts2[0].labels["testval"], "2"); 56 | 57 | let lc3 = Client::new("127.0.0.1", None, delay, Some("systemnope".to_string())); 58 | let hosts3 = lc3.get_hosts()?; 59 | assert_eq!(hosts3.len(), 0); 60 | 61 | host1.shutdown()?; 62 | host2.shutdown()?; 63 | std::thread::sleep(delay); 64 | Ok(()) 65 | } 66 | 67 | pub(crate) fn lattice_events() -> Result<(), Box> { 68 | use crossbeam_channel::unbounded; 69 | use latticeclient::{BusEvent, CloudEvent}; 70 | use std::time::Duration; 71 | use wascc_host::Host; 72 | 73 | let (s, r) = unbounded(); 74 | let nc = nats::connect("127.0.0.1")?; 75 | 76 | let _sub = nc.subscribe("wasmbus.events")?.with_handler(move |msg| { 77 | let ce: CloudEvent = serde_json::from_slice(&msg.data).unwrap(); 78 | let be: BusEvent = serde_json::from_str(&ce.data).unwrap(); 79 | let _ = s.send(be); 80 | Ok(()) 81 | }); 82 | 83 | // K/V counter host: 84 | // add_actor x 2 85 | // add_native_capability 86 | // bind_actor x 2 87 | let host = crate::common::gen_kvcounter_host(3666, Host::new())?; 88 | let delay = Duration::from_millis(500); 89 | std::thread::sleep(delay); 90 | host.shutdown()?; 91 | std::thread::sleep(delay); 92 | nc.close(); 93 | 94 | // * While these events are _mostly_ in deterministic order, because of the nature 95 | // of the fact that the capability providers are on background threads and 96 | // other events are issued by foreground threads, we can see certain events 97 | // appear "out of order", which makes for frustrating CI builds, so we do a 98 | // "contains" check here instead of checking on the exact delivery order. 99 | 100 | let a = [ 101 | r.recv().unwrap(), // host started 102 | r.recv().unwrap(), // extras prov loaded 103 | r.recv().unwrap(), // actor starting 104 | r.recv().unwrap(), // actor started 105 | r.recv().unwrap(), // prov loaded 106 | r.recv().unwrap(), // prov loaded 107 | r.recv().unwrap(), // binding created 108 | r.recv().unwrap(), // binding created 109 | // -- shut down 110 | r.recv().unwrap(), // actor stopped 111 | r.recv().unwrap(), // provider removed 112 | r.recv().unwrap(), // provider removed 113 | r.recv().unwrap(), // provider removed (remember "extras" is an omnipresent provider) 114 | r.recv().unwrap(), // host stop -- the last gasp 115 | ]; 116 | 117 | assert!(a.contains(&BusEvent::HostStarted(host.id()))); 118 | assert!(a.contains(&BusEvent::ProviderLoaded { 119 | capid: "wascc:extras".to_string(), 120 | instance_name: "default".to_string(), 121 | host: host.id(), 122 | })); 123 | assert!(a.contains(&BusEvent::ActorStarting { 124 | actor: "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ".to_string(), 125 | host: host.id(), 126 | })); 127 | assert!(a.contains(&BusEvent::ActorStarted { 128 | actor: "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ".to_string(), 129 | host: host.id(), 130 | })); 131 | assert!(a.contains(&BusEvent::ProviderLoaded { 132 | capid: "wascc:http_server".to_string(), 133 | instance_name: "default".to_string(), 134 | host: host.id(), 135 | })); 136 | assert!(a.contains(&BusEvent::ProviderLoaded { 137 | capid: "wascc:keyvalue".to_string(), 138 | instance_name: "default".to_string(), 139 | host: host.id(), 140 | })); 141 | assert!(a.contains(&BusEvent::ActorBindingCreated { 142 | actor: "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ".to_string(), 143 | capid: "wascc:keyvalue".to_string(), 144 | instance_name: "default".to_string(), 145 | host: host.id(), 146 | })); 147 | assert!(a.contains(&BusEvent::ActorBindingCreated { 148 | actor: "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ".to_string(), 149 | capid: "wascc:http_server".to_string(), 150 | instance_name: "default".to_string(), 151 | host: host.id(), 152 | })); 153 | assert!(a.contains(&BusEvent::ActorStopped { 154 | actor: "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ".to_string(), 155 | host: host.id(), 156 | })); 157 | 158 | assert!(a.contains(&BusEvent::ProviderRemoved { 159 | capid: "wascc:http_server".to_string(), 160 | host: host.id(), 161 | instance_name: "default".to_string(), 162 | })); 163 | assert!(a.contains(&BusEvent::ProviderRemoved { 164 | capid: "wascc:keyvalue".to_string(), 165 | host: host.id(), 166 | instance_name: "default".to_string(), 167 | })); 168 | assert!(a.contains(&BusEvent::ProviderRemoved { 169 | capid: "wascc:extras".to_string(), 170 | host: host.id(), 171 | instance_name: "default".to_string(), 172 | })); 173 | assert!(a.contains(&BusEvent::HostStopped(host.id()))); 174 | Ok(()) 175 | } 176 | 177 | pub(crate) fn can_bind_from_any_host() -> Result<(), Box> { 178 | use redis::Commands; 179 | use std::time::Duration; 180 | use wascc_host::{Actor, Host, HostBuilder, NativeCapability}; 181 | 182 | let port = 6203_u16; 183 | let host = HostBuilder::new() 184 | .with_lattice_namespace("bindanywhere") 185 | .build(); 186 | 187 | host.add_actor(Actor::from_file("./examples/.assets/kvcounter.wasm")?)?; 188 | host.add_native_capability(NativeCapability::from_file( 189 | "./examples/.assets/libwascc_httpsrv.so", 190 | None, 191 | )?)?; 192 | host.add_native_capability(NativeCapability::from_file( 193 | "./examples/.assets/libwascc_redis.so", 194 | None, 195 | )?)?; 196 | 197 | // Bindings in lattice mode should have a global scope, so we should 198 | // be able to invoke "set binding" from any host in the lattice 199 | // and have the binding take effect. 200 | let host2 = HostBuilder::new() 201 | .with_lattice_namespace("bindanywhere") 202 | .build(); 203 | host2.set_binding( 204 | "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ", 205 | "wascc:keyvalue", 206 | None, 207 | crate::common::redis_config(), 208 | )?; 209 | host2.set_binding( 210 | "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ", 211 | "wascc:http_server", 212 | None, 213 | crate::common::generate_port_config(port), 214 | )?; 215 | 216 | // Pretend to "move" the actor from one host to another 217 | // pre-existing binding should remain unaffected 218 | // NOTE: if you remove the last instance of an actor from a lattice, then 219 | // it will take out all of the bindings for that actor... so to keep that from 220 | // happening we add a second actor first, then remove the other one. 221 | host2.add_actor(Actor::from_file("./examples/.assets/kvcounter.wasm")?)?; 222 | std::thread::sleep(::std::time::Duration::from_millis(300)); 223 | host.remove_actor("MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ")?; 224 | std::thread::sleep(::std::time::Duration::from_millis(300)); 225 | println!("Host actor count {}", host.actors().len()); 226 | 227 | let key = uuid::Uuid::new_v4().to_string(); 228 | let rkey = format!(":{}", key); // the kv wasm logic does a replace on '/' with ':' 229 | let url = format!("http://localhost:6203/{}", key); 230 | let client = redis::Client::open("redis://127.0.0.1/")?; 231 | let mut con = client.get_connection()?; 232 | 233 | let mut resp = reqwest::blocking::get(&url)?; 234 | assert!(resp.status().is_success()); 235 | reqwest::blocking::get(&url)?; 236 | resp = reqwest::blocking::get(&url)?; // counter should be at 3 now 237 | assert!(resp.status().is_success()); 238 | assert_eq!(resp.text()?, "{\"counter\":3}"); 239 | host.shutdown()?; 240 | host2.shutdown()?; 241 | std::thread::sleep(Duration::from_millis(500)); 242 | 243 | let _: () = con.del(&rkey)?; 244 | Ok(()) 245 | } 246 | 247 | pub(crate) fn instance_count() -> Result<(), Box> { 248 | use latticeclient::{BusEvent, Client, CloudEvent}; 249 | use redis::Commands; 250 | use std::time::Duration; 251 | use wascc_host::{Actor, Host, HostBuilder, NativeCapability}; 252 | 253 | let port = 6209_u16; 254 | 255 | let host1 = HostBuilder::new().with_lattice_namespace("icount").build(); 256 | host1.add_actor(Actor::from_file("./examples/.assets/kvcounter.wasm")?)?; 257 | host1.add_native_capability(NativeCapability::from_file( 258 | "./examples/.assets/libwascc_httpsrv.so", 259 | None, 260 | )?)?; 261 | host1.add_native_capability(NativeCapability::from_file( 262 | "./examples/.assets/libwascc_redis.so", 263 | None, 264 | )?)?; 265 | 266 | host1.set_binding( 267 | "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ", 268 | "wascc:keyvalue", 269 | None, 270 | crate::common::redis_config(), 271 | )?; 272 | host1.set_binding( 273 | "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ", 274 | "wascc:http_server", 275 | None, 276 | crate::common::generate_port_config(port), 277 | )?; 278 | 279 | let host2 = HostBuilder::new().with_lattice_namespace("icount").build(); 280 | host2.add_actor(Actor::from_file("./examples/.assets/kvcounter.wasm")?)?; 281 | 282 | host2.add_native_capability(NativeCapability::from_file( 283 | "./examples/.assets/libwascc_redis.so", 284 | None, 285 | )?)?; 286 | 287 | std::thread::sleep(Duration::from_millis(300)); 288 | 289 | let nc = nats::connect("127.0.0.1")?; 290 | let lc = Client::with_connection(nc, Duration::from_secs(1), Some("icount".to_string())); 291 | let actors = lc.get_actors()?; 292 | let anames: Vec<_> = actors 293 | .values() 294 | .flatten() 295 | .map(|a| a.subject.to_string()) 296 | .collect(); 297 | assert_eq!(2, anames.len()); 298 | 299 | let count = match lc.get_actors() { 300 | Ok(res) => { 301 | let count = res.values().into_iter().fold(0, |acc, x| { 302 | acc + x 303 | .iter() 304 | .filter(|c| { 305 | c.subject == "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ" 306 | }) 307 | .collect::>() 308 | .len() 309 | }); 310 | count 311 | } 312 | Err(e) => 99, 313 | }; 314 | assert_eq!(count, 2); 315 | 316 | host1.shutdown()?; 317 | host2.shutdown()?; 318 | 319 | std::thread::sleep(Duration::from_millis(300)); 320 | Ok(()) 321 | } 322 | 323 | pub(crate) fn unload_reload_actor_retains_bindings() -> Result<(), Box> { 324 | use latticeclient::{BusEvent, CloudEvent}; 325 | use redis::Commands; 326 | use std::time::Duration; 327 | use wascc_host::{Actor, Host, HostBuilder, NativeCapability}; 328 | 329 | let nc = nats::connect("127.0.0.1")?; 330 | 331 | let port = 6201_u16; 332 | 333 | let host1 = HostBuilder::new() 334 | .with_lattice_namespace("unloadreload") 335 | .with_label("testval", "1") 336 | .build(); 337 | host1.add_actor(Actor::from_file("./examples/.assets/kvcounter.wasm")?)?; 338 | host1.add_native_capability(NativeCapability::from_file( 339 | "./examples/.assets/libwascc_httpsrv.so", 340 | None, 341 | )?)?; 342 | host1.add_native_capability(NativeCapability::from_file( 343 | "./examples/.assets/libwascc_redis.so", 344 | None, 345 | )?)?; 346 | 347 | // *** !! 348 | // These bindings only need to be set once per lattice, not once per host 349 | // *** !! 350 | host1.set_binding( 351 | "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ", 352 | "wascc:keyvalue", 353 | None, 354 | crate::common::redis_config(), 355 | )?; 356 | host1.set_binding( 357 | "MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ", 358 | "wascc:http_server", 359 | None, 360 | crate::common::generate_port_config(port), 361 | )?; 362 | 363 | let host2 = HostBuilder::new() 364 | .with_lattice_namespace("unloadreload") 365 | .with_label("testval", "2") 366 | .build(); 367 | host2.add_actor(Actor::from_file("./examples/.assets/kvcounter.wasm")?)?; 368 | 369 | // The actor in host 2 should immediately be able to talk to this redis 370 | // provider because of the re-establish binding code 371 | host2.add_native_capability(NativeCapability::from_file( 372 | "./examples/.assets/libwascc_redis.so", 373 | None, 374 | )?)?; 375 | std::thread::sleep(Duration::from_millis(500)); 376 | 377 | let key = uuid::Uuid::new_v4().to_string(); 378 | let rkey = format!(":{}", key); // the kv wasm logic does a replace on '/' with ':' 379 | let url = format!("http://localhost:{}/{}", port, key); 380 | 381 | // Invoke it enough times to ensure that both actors get exercised... 382 | let mut resp = reqwest::blocking::get(&url)?; 383 | assert!(resp.status().is_success()); 384 | reqwest::blocking::get(&url)?; 385 | reqwest::blocking::get(&url)?; // counter should be at 3 now 386 | 387 | host2.remove_actor("MASCXFM4R6X63UD5MSCDZYCJNPBVSIU6RKMXUPXRKAOSBQ6UY3VT3NPZ")?; 388 | std::thread::sleep(Duration::from_millis(500)); // give actor thread time to terminate 389 | 390 | reqwest::blocking::get(&url)?; 391 | resp = reqwest::blocking::get(&url)?; // counter should be at 5 now 392 | assert!(resp.status().is_success()); 393 | assert_eq!(resp.text()?, "{\"counter\":5}"); 394 | 395 | host2.add_actor(Actor::from_file("./examples/.assets/kvcounter.wasm")?)?; 396 | std::thread::sleep(Duration::from_millis(500)); 397 | // should not have to call re-bind here, because the removed actor was not the last of its kind in the lattice 398 | 399 | reqwest::blocking::get(&url)?; 400 | resp = reqwest::blocking::get(&url)?; // counter should be at 7 now 401 | assert!(resp.status().is_success()); 402 | assert_eq!(resp.text()?, "{\"counter\":7}"); 403 | 404 | // Purge the counter value from Redis 405 | let client = redis::Client::open("redis://127.0.0.1/")?; 406 | let mut con = client.get_connection()?; 407 | let _: () = con.del(&rkey)?; 408 | assert_eq!(1, host2.actors().len()); 409 | assert_eq!(1, host1.actors().len()); 410 | 411 | host1.shutdown()?; 412 | host2.shutdown()?; 413 | std::thread::sleep(Duration::from_millis(500)); 414 | 415 | Ok(()) 416 | } 417 | -------------------------------------------------------------------------------- /tests/lib.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | mod auth; 4 | mod common; 5 | mod core; 6 | #[cfg(feature = "lattice")] 7 | mod lattice; 8 | mod load; 9 | 10 | #[test] 11 | fn default_authorizer_enforces_cap_attestations() -> Result<(), Box> { 12 | auth::default_authorizer_enforces_cap_attestations() 13 | } 14 | 15 | #[test] 16 | fn authorizer_blocks_bindings() -> Result<(), Box> { 17 | auth::authorizer_blocks_bindings() 18 | } 19 | 20 | #[test] 21 | fn authorizer_blocks_load() -> Result<(), Box> { 22 | auth::authorizer_blocks_load() 23 | } 24 | 25 | #[test] 26 | fn stock_host() -> Result<(), Box> { 27 | core::stock_host() 28 | } 29 | 30 | #[test] 31 | fn kv_host() -> Result<(), Box> { 32 | core::kv_host() 33 | } 34 | 35 | #[test] 36 | #[cfg(feature = "lattice")] 37 | fn unload_reload_actor_retains_bindings() -> Result<(), Box> { 38 | lattice::unload_reload_actor_retains_bindings() 39 | } 40 | 41 | #[test] 42 | #[cfg(feature = "lattice")] 43 | fn can_bind_from_any_host() -> Result<(), Box> { 44 | lattice::can_bind_from_any_host() 45 | } 46 | 47 | #[test] 48 | #[cfg(feature = "lattice")] 49 | fn instance_count() -> Result<(), Box> { 50 | lattice::instance_count() 51 | } 52 | 53 | #[test] 54 | #[cfg(feature = "lattice")] 55 | fn lattice_single_host() -> Result<(), Box> { 56 | lattice::lattice_single_host() 57 | } 58 | 59 | #[test] 60 | #[cfg(feature = "lattice")] 61 | fn lattice_isolation() -> Result<(), Box> { 62 | lattice::lattice_isolation() 63 | } 64 | 65 | #[test] 66 | #[cfg(feature = "lattice")] 67 | fn lattice_events() -> Result<(), Box> { 68 | lattice::lattice_events() 69 | } 70 | 71 | //#[test] 72 | //fn simple_load() -> Result<(), Box> { 73 | // load::simple_load() 74 | //} 75 | 76 | #[test] 77 | fn actor_only_load() -> Result<(), Box> { 78 | load::actor_only_load() 79 | } 80 | -------------------------------------------------------------------------------- /tests/load.rs: -------------------------------------------------------------------------------- 1 | use crate::common; 2 | use std::error::Error; 3 | use std::time::Instant; 4 | use wascc_host::{Actor, Host, HostBuilder, NativeCapability}; 5 | 6 | pub(crate) fn actor_only_load() -> Result<(), Box> { 7 | #[cfg(feature = "lattice")] 8 | let host = HostBuilder::new() 9 | .with_lattice_namespace("actoronlyload") 10 | .build(); 11 | 12 | #[cfg(not(feature = "lattice"))] 13 | let host = HostBuilder::new().build(); 14 | 15 | host.add_native_capability(NativeCapability::from_file( 16 | "./examples/.assets/libwascc_httpsrv.so", 17 | None, 18 | )?)?; 19 | host.add_native_capability(NativeCapability::from_file( 20 | "./examples/.assets/libwascc_redis.so", 21 | None, 22 | )?)?; 23 | 24 | use std::fs::File; 25 | use std::io::Read; 26 | 27 | let bytes = { 28 | let mut f = File::open("./examples/.assets/kvcounter.wasm")?; 29 | let mut bytes = Vec::new(); 30 | f.read_to_end(&mut bytes)?; 31 | bytes 32 | }; 33 | 34 | let start = Instant::now(); 35 | // Change this value to run heavier loads on your workstation. It's 36 | // low to keep the CI builds from crashing 37 | for i in 0..50 { 38 | let a = common::generate_resigned_actor(&bytes)?; 39 | let actor_id = a.public_key(); 40 | host.add_actor(a)?; 41 | if (i % 10 == 0) { 42 | println!("Actor {} added", i); 43 | } 44 | } 45 | let elapsed = start.elapsed().as_secs(); 46 | println!( 47 | "Finished loading actors: {} seconds ({} avg)", 48 | elapsed, 49 | elapsed / 50 50 | ); 51 | assert_eq!(50, host.actors().len()); 52 | host.shutdown()?; 53 | Ok(()) 54 | } 55 | 56 | pub(crate) fn simple_load() -> Result<(), Box> { 57 | use redis::Commands; 58 | 59 | #[cfg(feature = "lattice")] 60 | let host = HostBuilder::new() 61 | .with_lattice_namespace("simpleload") 62 | .build(); 63 | 64 | #[cfg(not(feature = "lattice"))] 65 | let host = HostBuilder::new().build(); 66 | 67 | host.add_native_capability(NativeCapability::from_file( 68 | "./examples/.assets/libwascc_httpsrv.so", 69 | None, 70 | )?)?; 71 | host.add_native_capability(NativeCapability::from_file( 72 | "./examples/.assets/libwascc_redis.so", 73 | None, 74 | )?)?; 75 | 76 | use std::fs::File; 77 | use std::io::Read; 78 | 79 | let bytes = { 80 | let mut f = File::open("./examples/.assets/kvcounter.wasm")?; 81 | let mut bytes = Vec::new(); 82 | f.read_to_end(&mut bytes)?; 83 | bytes 84 | }; 85 | 86 | let base_port = 32000; 87 | for i in 0..100 { 88 | let a = common::generate_resigned_actor(&bytes)?; 89 | let actor_id = a.public_key(); 90 | host.add_actor(a)?; 91 | 92 | host.set_binding( 93 | &actor_id, 94 | "wascc:http_server", 95 | None, 96 | common::generate_port_config(base_port + i), 97 | )?; 98 | host.set_binding(&actor_id, "wascc:keyvalue", None, common::redis_config())?; 99 | } 100 | println!("** LOADED ALL ACTORS ** "); 101 | 102 | // Query all the HTTP ports 103 | for i in 0..100 { 104 | let key = format!("loadcounter{}", i); 105 | let url = format!("http://localhost:{}/{}", base_port + i, key); 106 | let mut resp = reqwest::blocking::get(&url)?; 107 | assert!(resp.status().is_success()); 108 | let counter: serde_json::Value = serde_json::from_str(&resp.text()?)?; 109 | 110 | let expected: i64 = i as i64 + 1; 111 | assert_eq!(expected, counter["counter"].as_i64().unwrap()); 112 | } 113 | 114 | host.shutdown()?; 115 | 116 | let client = redis::Client::open("redis://127.0.0.1/")?; 117 | let mut con = client.get_connection()?; 118 | for i in 0..100 { 119 | let rkey = format!("loadcounter{}", i); 120 | let _: () = con.del(&rkey)?; 121 | } 122 | 123 | Ok(()) 124 | } 125 | --------------------------------------------------------------------------------