├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── cli ├── Cargo.toml ├── src │ ├── main.rs │ └── opts.rs └── tests │ ├── integration │ ├── acl.rs │ ├── args.rs │ ├── async_io.rs │ ├── body.rs │ ├── cache.rs │ ├── client_certs.rs │ ├── common.rs │ ├── common │ │ └── backends.rs │ ├── config_store_lookup.rs │ ├── device_detection_lookup.rs │ ├── dictionary_lookup.rs │ ├── downstream_req.rs │ ├── edge_rate_limiting.rs │ ├── env_vars.rs │ ├── geolocation_lookup.rs │ ├── grpc.rs │ ├── hello_world.gzip │ ├── http_semantics.rs │ ├── inspect.rs │ ├── kv_store.rs │ ├── logging.rs │ ├── main.rs │ ├── memory.rs │ ├── request.rs │ ├── response.rs │ ├── secret_store.rs │ ├── sending_response.rs │ ├── shielding.rs │ ├── sleep.rs │ ├── unknown_import_behavior.rs │ ├── upstream.rs │ ├── upstream_async.rs │ ├── upstream_dynamic.rs │ ├── upstream_streaming.rs │ └── vcpu_time.rs │ ├── trap-test │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ │ └── main.rs │ └── wasm │ ├── exit_code_4.wat │ ├── invalid.wat │ ├── minimal.wasm │ ├── minimal.wat │ └── trapping.wat ├── crates └── adapter │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ ├── byte-array-literals │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs │ └── src │ ├── descriptors.rs │ ├── fastly │ ├── cache.rs │ ├── config_store.rs │ ├── core.rs │ ├── error.rs │ ├── http_cache.rs │ ├── macros.rs │ └── mod.rs │ ├── lib.rs │ └── macros.rs ├── doc ├── RELEASING.md └── logo.png ├── lib ├── Cargo.toml ├── compute-at-edge-abi │ ├── README.md │ ├── cache.witx │ ├── compute-at-edge.witx │ ├── config-store.witx │ ├── http-cache.witx │ ├── shielding.witx │ └── typenames.witx ├── data │ └── viceroy-component-adapter.wasm ├── proptest-regressions │ └── cache.txt ├── src │ ├── acl.rs │ ├── adapt.rs │ ├── async_io.rs │ ├── body.rs │ ├── cache.rs │ ├── cache │ │ ├── store.rs │ │ └── variance.rs │ ├── collecting_body.rs │ ├── component │ │ ├── acl.rs │ │ ├── async_io.rs │ │ ├── backend.rs │ │ ├── cache.rs │ │ ├── compute_runtime.rs │ │ ├── config_store.rs │ │ ├── device_detection.rs │ │ ├── dictionary.rs │ │ ├── erl.rs │ │ ├── error.rs │ │ ├── geo.rs │ │ ├── headers.rs │ │ ├── http_body.rs │ │ ├── http_req.rs │ │ ├── http_resp.rs │ │ ├── http_types.rs │ │ ├── image_optimizer.rs │ │ ├── kv_store.rs │ │ ├── log.rs │ │ ├── mod.rs │ │ ├── object_store.rs │ │ ├── purge.rs │ │ ├── secret_store.rs │ │ ├── shielding.rs │ │ ├── types.rs │ │ └── uap.rs │ ├── config.rs │ ├── config │ │ ├── acl.rs │ │ ├── backends.rs │ │ ├── backends │ │ │ └── client_cert_info.rs │ │ ├── device_detection.rs │ │ ├── dictionaries.rs │ │ ├── geolocation.rs │ │ ├── limits.rs │ │ ├── object_store.rs │ │ ├── secret_store.rs │ │ └── unit_tests.rs │ ├── downstream.rs │ ├── error.rs │ ├── execute.rs │ ├── headers.rs │ ├── lib.rs │ ├── linking.rs │ ├── logging.rs │ ├── object_store.rs │ ├── secret_store.rs │ ├── service.rs │ ├── session.rs │ ├── session │ │ ├── async_item.rs │ │ └── downstream.rs │ ├── shielding_site.rs │ ├── streaming_body.rs │ ├── upstream.rs │ ├── wiggle_abi.rs │ └── wiggle_abi │ │ ├── acl.rs │ │ ├── backend_impl.rs │ │ ├── body_impl.rs │ │ ├── cache.rs │ │ ├── compute_runtime.rs │ │ ├── config_store.rs │ │ ├── device_detection_impl.rs │ │ ├── dictionary_impl.rs │ │ ├── entity.rs │ │ ├── erl_impl.rs │ │ ├── fastly_purge_impl.rs │ │ ├── geo_impl.rs │ │ ├── headers.rs │ │ ├── http_cache.rs │ │ ├── image_optimizer.rs │ │ ├── kv_store_impl.rs │ │ ├── log_impl.rs │ │ ├── obj_store_impl.rs │ │ ├── req_impl.rs │ │ ├── resp_impl.rs │ │ ├── secret_store_impl.rs │ │ ├── shielding.rs │ │ └── uap_impl.rs └── wit │ ├── deps │ ├── cli │ │ ├── command.wit │ │ ├── environment.wit │ │ ├── exit.wit │ │ ├── imports.wit │ │ ├── run.wit │ │ ├── stdio.wit │ │ └── terminal.wit │ ├── clocks │ │ ├── monotonic-clock.wit │ │ ├── wall-clock.wit │ │ └── world.wit │ ├── fastly │ │ └── compute.wit │ ├── filesystem │ │ ├── preopens.wit │ │ ├── types.wit │ │ └── world.wit │ ├── http │ │ ├── handler.wit │ │ ├── proxy.wit │ │ └── types.wit │ ├── io │ │ ├── error.wit │ │ ├── poll.wit │ │ ├── streams.wit │ │ └── world.wit │ ├── random │ │ ├── insecure-seed.wit │ │ ├── insecure.wit │ │ ├── random.wit │ │ └── world.wit │ └── sockets │ │ ├── instance-network.wit │ │ ├── ip-name-lookup.wit │ │ ├── network.wit │ │ ├── tcp-create-socket.wit │ │ ├── tcp.wit │ │ ├── udp-create-socket.wit │ │ ├── udp.wit │ │ └── world.wit │ └── viceroy.wit ├── rust-toolchain ├── rustfmt.toml ├── scripts └── publish.rs └── test-fixtures ├── Cargo.lock ├── Cargo.toml ├── combined_heap_limits.wat ├── data ├── ca.key ├── ca.pem ├── ca.srl ├── client.crt ├── client.csr ├── client.key ├── device-detection-mapping.json ├── geolocation-mapping.json ├── hello_world.gz ├── json-config_store.json ├── json-dictionary.json ├── json-kv_store-invalid_1.json ├── json-kv_store-invalid_2.json ├── json-kv_store.json ├── json-secret_store.json ├── kv-store.txt ├── my-acl-1.json ├── my-acl-2.json ├── server.crt ├── server.csr ├── server.ext └── server.key ├── return_ok.wat └── src ├── bin ├── acl.rs ├── args.rs ├── async_io.rs ├── bad-framing-headers.rs ├── cache.rs ├── config-store-lookup.rs ├── content-length.rs ├── device-detection-lookup.rs ├── dictionary-lookup.rs ├── downstream-req.rs ├── edge-rate-limiting.rs ├── env-vars.rs ├── expects-hello.rs ├── geolocation-lookup-default.rs ├── geolocation-lookup.rs ├── grpc.rs ├── gzipped-response.rs ├── inspect.rs ├── kv_store.rs ├── logging.rs ├── mutual-tls.rs ├── noop.rs ├── panic.rs ├── request.rs ├── response.rs ├── secret-store.rs ├── shielding.rs ├── sleep.rs ├── streaming-response.rs ├── teapot-status.rs ├── unknown-import.rs ├── upstream-async.rs ├── upstream-dynamic.rs ├── upstream-streaming.rs ├── upstream.rs ├── vcpu_time_test.rs ├── write-and-read-body.rs └── write-body.rs └── limits.rs /.gitignore: -------------------------------------------------------------------------------- 1 | **/target/ 2 | publish 3 | vendor/ 4 | verify-publishable/ 5 | .vscode 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "cli", 4 | "lib", 5 | "crates/adapter", 6 | "crates/adapter/byte-array-literals", 7 | ] 8 | resolver = "2" 9 | 10 | # Exclude our integration test fixtures, which need to be compiled to wasm 11 | # (managed by the Makefile) 12 | exclude = [ 13 | "test-fixtures", 14 | ] 15 | 16 | # Specify `cli` as the default workspace member to operate on. This means that 17 | # commands like `cargo run` will run the CLI binary by default. 18 | # See: https://doc.rust-lang.org/cargo/reference/workspaces.html#package-selection 19 | default-members = [ "cli" ] 20 | 21 | [profile.dev] 22 | # Since some of the integration tests involve compiling Wasm, a little optimization goes a long way 23 | # toward making the test suite not take forever 24 | opt-level = 1 25 | 26 | [workspace.dependencies] 27 | anyhow = "1.0.31" 28 | base64 = "0.21.2" 29 | clap = { version = "^4.0.18", features = ["derive"] } 30 | hyper = { version = "=0.14.26", features = ["full"] } 31 | itertools = "0.10.5" 32 | pin-project = "1.0.8" 33 | rustls = { version = "0.21.5", features = ["dangerous_configuration"] } 34 | rustls-pemfile = "1.0.3" 35 | serde_json = "1.0.59" 36 | tokio = { version = "1.21.2", features = ["full"] } 37 | tokio-rustls = "0.24.1" 38 | tracing = "0.1.37" 39 | tracing-futures = "0.2.5" 40 | futures = "0.3.24" 41 | url = "2.3.1" 42 | 43 | # Wasmtime dependencies 44 | wasmtime = { version = "25.0.0", features = ["call-hook"] } 45 | wasmtime-wasi = "25.0.0" 46 | wasmtime-wasi-nn = "25.0.0" 47 | wiggle = "25.0.0" 48 | wat = "1.212.0" 49 | wasmparser = "0.217.0" 50 | wasm-encoder = { version = "0.217.0", features = ["wasmparser"] } 51 | wit-component = "0.217.0" 52 | 53 | # Adapter dependencies 54 | byte-array-literals = { path = "crates/adapter/byte-array-literals" } 55 | bitflags = { version = "2.5.0", default-features = false } 56 | object = { version = "0.33", default-features = false, features = ["archive"] } 57 | wasi = { version = "0.11.0", default-features = false } 58 | wit-bindgen-rust-macro = { version = "0.32.0", default-features = false } 59 | 60 | [profile.release.package.viceroy-component-adapter] 61 | opt-level = 's' 62 | strip = 'debuginfo' 63 | 64 | [profile.dev.package.viceroy-component-adapter] 65 | # Make dev look like a release build since this adapter module won't work with 66 | # a debug build that uses data segments and such. 67 | incremental = false 68 | opt-level = 's' 69 | # Omit assertions, which include failure messages which require string 70 | # initializers. 71 | debug-assertions = false 72 | # Omit integer overflow checks, which include failure messages which require 73 | # string initializers. 74 | overflow-checks = false 75 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:bionic 2 | 3 | ARG VICEROY_SRC=/Viceroy 4 | 5 | ENV DEBIAN_FRONTEND=noninteractive 6 | 7 | RUN apt-get update \ 8 | && apt-get install -y --no-install-recommends \ 9 | build-essential \ 10 | curl \ 11 | git \ 12 | ca-certificates \ 13 | pkg-config \ 14 | libssl-dev 15 | 16 | # Setting a consistent LD_LIBRARY_PATH across the entire environment prevents 17 | # unnecessary Cargo rebuilds. 18 | ENV LD_LIBRARY_PATH=/usr/local/lib 19 | 20 | # Install Rust, rustfmt, and the wasm32-wasi cross-compilation target 21 | RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.52.1 -y 22 | ENV PATH=/root/.cargo/bin:$PATH 23 | RUN rustup component add rustfmt 24 | RUN rustup target add wasm32-wasip1 25 | 26 | WORKDIR $VICEROY_SRC 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Default to using regular `cargo`. CI targets may override this. 2 | VICEROY_CARGO=cargo 3 | 4 | .PHONY: format 5 | format: 6 | $(VICEROY_CARGO) fmt 7 | 8 | .PHONY: format-check 9 | format-check: 10 | $(VICEROY_CARGO) fmt -- --check 11 | 12 | .PHONY: clippy 13 | clippy: 14 | $(VICEROY_CARGO) clippy --all -- -D warnings 15 | 16 | .PHONY: test 17 | test: test-crates trap-test 18 | 19 | .PHONY: test-crates 20 | test-crates: fix-build 21 | RUST_BACKTRACE=1 $(VICEROY_CARGO) test --all 22 | 23 | .PHONY: fix-build 24 | fix-build: 25 | cd test-fixtures && $(VICEROY_CARGO) build --target=wasm32-wasip1 26 | 27 | .PHONY: trap-test 28 | trap-test: fix-build 29 | cd cli/tests/trap-test && RUST_BACKTRACE=1 $(VICEROY_CARGO) test fatal_error_traps -- --nocapture 30 | 31 | # The `trap-test` is its own top-level target for CI in order to achieve better build parallelism. 32 | .PHONY: trap-test-ci 33 | trap-test-ci: VICEROY_CARGO=cargo --locked 34 | trap-test-ci: trap-test 35 | 36 | # The main `ci` target runs everything except `trap-test`. 37 | .PHONY: ci 38 | ci: VICEROY_CARGO=cargo --locked 39 | ci: format-check test-crates 40 | 41 | .PHONY: clean 42 | clean: 43 | $(VICEROY_CARGO) clean 44 | cd cli/tests/trap-test/ && $(VICEROY_CARGO) clean 45 | 46 | # Open the documentation for the workspace in a browser. 47 | .PHONY: doc 48 | doc: 49 | $(VICEROY_CARGO) doc --workspace --open 50 | 51 | # Open the documentation for the workspace in a browser. 52 | # 53 | # Note: This includes private items, which can be useful for development. 54 | .PHONY: doc-dev 55 | doc-dev: 56 | $(VICEROY_CARGO) doc --no-deps --document-private-items --workspace --open 57 | 58 | # Run `cargo generate-lockfile` for all of the crates in the project. 59 | .PHONY: generate-lockfile 60 | generate-lockfile: 61 | $(VICEROY_CARGO) generate-lockfile 62 | $(VICEROY_CARGO) generate-lockfile --manifest-path=test-fixtures/Cargo.toml 63 | $(VICEROY_CARGO) generate-lockfile --manifest-path=cli/tests/trap-test/Cargo.toml 64 | 65 | # Check that the crates can be packaged for crates.io. 66 | # 67 | # FIXME(katie): Add option flags to `publish.rs` for the vendor directory, remove `.cargo/` after 68 | # running. 69 | .PHONY: package-check 70 | package-check: 71 | rustc scripts/publish.rs 72 | ./publish verify 73 | rm publish 74 | rm -rf .cargo/ 75 | rm -rf verify-publishable/ 76 | 77 | # Re-generate the adapter, and move it into `lib/adapter` 78 | .PHONY: adapter 79 | adapter: 80 | cargo build --release \ 81 | -p viceroy-component-adapter \ 82 | --target wasm32-unknown-unknown 83 | mkdir -p lib/adapter 84 | cp target/wasm32-unknown-unknown/release/viceroy_component_adapter.wasm \ 85 | lib/data/viceroy-component-adapter.wasm 86 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Report a security issue 2 | 3 | The fastly/Viceroy project team welcomes security reports and is committed to providing prompt attention to security issues. Security issues should be reported privately via [Fastly’s security issue reporting process](https://www.fastly.com/security/report-security-issue). 4 | 5 | ## Security advisories 6 | 7 | Remediation of security vulnerabilities is prioritized by the project team. The project team endeavors to coordinate remediation with third-party stakeholders, and is committed to transparency in the disclosure process. The fastly/Viceroy team announces security issues in release notes as well as Github Security Advisories on a best-effort basis. 8 | 9 | Note that communications related to security issues in Fastly-maintained OSS as described here are distinct from [Fastly Security Advisories](https://www.fastly.com/security-advisories). 10 | 11 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "viceroy" 3 | description = "Viceroy is a local testing daemon for Fastly Compute." 4 | version = "0.13.1" 5 | authors = ["Fastly"] 6 | readme = "../README.md" 7 | edition = "2021" 8 | license = "Apache-2.0 WITH LLVM-exception" 9 | documentation = "https://developer.fastly.com/learning/compute/testing/#running-a-local-testing-server" 10 | homepage = "https://developer.fastly.com/learning/compute/" 11 | repository = "https://github.com/fastly/Viceroy" 12 | keywords = ["wasm", "fastly"] 13 | categories = [ 14 | "command-line-utilities", 15 | "development-tools", 16 | "network-programming", 17 | "simulation", 18 | "wasm" 19 | ] 20 | include = [ 21 | "../README.md", 22 | "../CHANGELOG.md", 23 | "../SECURITY.md", 24 | "../doc/logo.png", 25 | "src/**/*" 26 | ] 27 | # MSRV: 1.78 for wasm32-wasip1 28 | rust-version = "1.78" 29 | 30 | [[bin]] 31 | name = "viceroy" 32 | path = "src/main.rs" 33 | 34 | [dependencies] 35 | anyhow = { workspace = true } 36 | base64 = { workspace = true } 37 | hyper = { workspace = true } 38 | itertools = { workspace = true } 39 | serde_json = { workspace = true } 40 | serial_test = "^2.0.0" 41 | clap = { workspace = true } 42 | rustls = { workspace = true } 43 | rustls-pemfile = { workspace = true } 44 | tls-listener = { version = "^0.7.0", features = ["rustls", "hyper-h1", "tokio-net", "rt"] } 45 | tokio = { workspace = true } 46 | tokio-rustls = { workspace = true } 47 | tracing = { workspace = true } 48 | tracing-futures = { workspace = true } 49 | tracing-subscriber = { version = "^0.3.16", features = ["env-filter", "fmt"] } 50 | viceroy-lib = { path = "../lib", version = "=0.13.1" } 51 | wat = "^1.0.38" 52 | wasmtime = { workspace = true } 53 | wasmtime-wasi = { workspace = true } 54 | libc = "^0.2.139" 55 | 56 | [dev-dependencies] 57 | anyhow = { workspace = true } 58 | futures = { workspace = true } 59 | url = { workspace = true } 60 | -------------------------------------------------------------------------------- /cli/tests/integration/args.rs: -------------------------------------------------------------------------------- 1 | use crate::common::{Test, TestResult}; 2 | use hyper::{body::to_bytes, StatusCode}; 3 | 4 | /// Run a program that tests its args. This checks that we're populating the argument list with the 5 | /// singleton "compute-app" value. 6 | /// Check that an empty response is sent downstream by default. 7 | /// 8 | /// `args.wasm` is a guest program checks its cli args. 9 | #[tokio::test(flavor = "multi_thread")] 10 | async fn empty_ok_response_by_default_after_args() -> TestResult { 11 | let resp = Test::using_fixture("args.wasm").against_empty().await?; 12 | 13 | assert_eq!(resp.status(), StatusCode::OK); 14 | assert!(to_bytes(resp.into_body()) 15 | .await 16 | .expect("can read body") 17 | .to_vec() 18 | .is_empty()); 19 | 20 | Ok(()) 21 | } 22 | 23 | /// Run a program that tests its args. This checks that we're populating the argument list with the 24 | /// singleton "compute-app" value. 25 | /// Check that an empty response is sent downstream by default. 26 | /// 27 | /// `args.wasm` is a guest program checks its cli args. 28 | #[tokio::test(flavor = "multi_thread")] 29 | async fn empty_ok_response_by_default_after_args_component() { 30 | let resp = Test::using_fixture("args.wasm") 31 | .adapt_component(true) 32 | .against_empty() 33 | .await 34 | .unwrap(); 35 | 36 | assert_eq!(resp.status(), StatusCode::OK); 37 | assert!(to_bytes(resp.into_body()) 38 | .await 39 | .expect("can read body") 40 | .to_vec() 41 | .is_empty()); 42 | } 43 | -------------------------------------------------------------------------------- /cli/tests/integration/cache.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | common::{Test, TestResult}, 4 | viceroy_test, 5 | }, 6 | hyper::StatusCode, 7 | }; 8 | 9 | viceroy_test!(cache, |is_component| { 10 | let resp = Test::using_fixture("cache.wasm") 11 | .adapt_component(is_component) 12 | .against_empty() 13 | .await?; 14 | assert_eq!(resp.status(), StatusCode::OK); 15 | Ok(()) 16 | }); 17 | -------------------------------------------------------------------------------- /cli/tests/integration/config_store_lookup.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | common::{Test, TestResult}, 3 | viceroy_test, 4 | }; 5 | use hyper::{body::to_bytes, StatusCode}; 6 | 7 | viceroy_test!(json_config_store_lookup_works, |is_component| { 8 | const FASTLY_TOML: &str = r#" 9 | name = "json-config_store-lookup" 10 | description = "json config_store lookup test" 11 | authors = ["Jill Bryson ", "Rose McDowall "] 12 | language = "rust" 13 | [local_server.config_stores.animals] 14 | file = "../test-fixtures/data/json-config_store.json" 15 | format = "json" 16 | "#; 17 | 18 | let resp = Test::using_fixture("config-store-lookup.wasm") 19 | .adapt_component(is_component) 20 | .using_fastly_toml(FASTLY_TOML)? 21 | .against_empty() 22 | .await?; 23 | 24 | assert_eq!(resp.status(), StatusCode::OK); 25 | assert!(to_bytes(resp.into_body()) 26 | .await 27 | .expect("can read body") 28 | .to_vec() 29 | .is_empty()); 30 | 31 | Ok(()) 32 | }); 33 | 34 | viceroy_test!(inline_toml_config_store_lookup_works, |is_component| { 35 | const FASTLY_TOML: &str = r#" 36 | name = "inline-toml-config_store-lookup" 37 | description = "inline toml config_store lookup test" 38 | authors = ["Jill Bryson ", "Rose McDowall "] 39 | language = "rust" 40 | [local_server.config_stores.animals] 41 | format = "inline-toml" 42 | [local_server.config_stores.animals.contents] 43 | dog = "woof" 44 | cat = "meow" 45 | "#; 46 | 47 | let resp = Test::using_fixture("config-store-lookup.wasm") 48 | .adapt_component(is_component) 49 | .using_fastly_toml(FASTLY_TOML)? 50 | .against_empty() 51 | .await?; 52 | 53 | assert_eq!(resp.status(), StatusCode::OK); 54 | assert!(to_bytes(resp.into_body()) 55 | .await 56 | .expect("can read body") 57 | .to_vec() 58 | .is_empty()); 59 | 60 | Ok(()) 61 | }); 62 | 63 | viceroy_test!(missing_config_store_works, |is_component| { 64 | const FASTLY_TOML: &str = r#" 65 | name = "missing-config_store-config" 66 | description = "missing config_store test" 67 | language = "rust" 68 | "#; 69 | 70 | let resp = Test::using_fixture("config-store-lookup.wasm") 71 | .adapt_component(is_component) 72 | .using_fastly_toml(FASTLY_TOML)? 73 | .against_empty() 74 | .await?; 75 | 76 | assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); 77 | 78 | Ok(()) 79 | }); 80 | -------------------------------------------------------------------------------- /cli/tests/integration/device_detection_lookup.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | common::{Test, TestResult}, 3 | viceroy_test, 4 | }; 5 | use hyper::{body::to_bytes, StatusCode}; 6 | 7 | viceroy_test!(json_device_detection_lookup_works, |is_component| { 8 | const FASTLY_TOML: &str = r#" 9 | name = "json-device-detection-lookup" 10 | description = "json device detection lookup test" 11 | authors = ["Test User "] 12 | language = "rust" 13 | [local_server] 14 | [local_server.device_detection] 15 | file = "../test-fixtures/data/device-detection-mapping.json" 16 | format = "json" 17 | "#; 18 | 19 | let resp = Test::using_fixture("device-detection-lookup.wasm") 20 | .adapt_component(is_component) 21 | .using_fastly_toml(FASTLY_TOML)? 22 | .against_empty() 23 | .await?; 24 | 25 | assert_eq!(resp.status(), StatusCode::OK); 26 | assert!(to_bytes(resp.into_body()) 27 | .await 28 | .expect("can read body") 29 | .to_vec() 30 | .is_empty()); 31 | 32 | Ok(()) 33 | }); 34 | 35 | viceroy_test!(inline_toml_device_detection_lookup_works, |is_component| { 36 | const FASTLY_TOML: &str = r#" 37 | name = "inline-toml-device-detection-lookup" 38 | description = "inline toml device detection lookup test" 39 | authors = ["Test User "] 40 | language = "rust" 41 | [local_server] 42 | [local_server.device_detection] 43 | format = "inline-toml" 44 | [local_server.device_detection.user_agents] 45 | [local_server.device_detection.user_agents."Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0 [FBAN/FBIOS;FBAV/8.0.0.28.18;FBBV/1665515;FBDV/iPhone4,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/7.0.4;FBSS/2; FBCR/Telekom.de;FBID/phone;FBLC/de_DE;FBOP/5]"] 46 | user_agent = {} 47 | os = {} 48 | device = {name = "iPhone", brand = "Apple", model = "iPhone4,1", hwtype = "Mobile Phone", is_ereader = false, is_gameconsole = false, is_mediaplayer = false, is_mobile = true, is_smarttv = false, is_tablet = false, is_tvplayer = false, is_desktop = false, is_touchscreen = true } 49 | [local_server.device_detection.user_agents."ghosts-app/1.0.2.1 (ASUSTeK COMPUTER INC.; X550CC; Windows 8 (X86); en)"] 50 | user_agent = {} 51 | os = {} 52 | device = {name = "Asus TeK", brand = "Asus", model = "TeK", is_desktop = false } 53 | "#; 54 | 55 | let resp = Test::using_fixture("device-detection-lookup.wasm") 56 | .adapt_component(is_component) 57 | .using_fastly_toml(FASTLY_TOML)? 58 | .against_empty() 59 | .await?; 60 | 61 | assert_eq!(resp.status(), StatusCode::OK); 62 | assert!(to_bytes(resp.into_body()) 63 | .await 64 | .expect("can read body") 65 | .to_vec() 66 | .is_empty()); 67 | 68 | Ok(()) 69 | }); 70 | -------------------------------------------------------------------------------- /cli/tests/integration/dictionary_lookup.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | common::{Test, TestResult}, 3 | viceroy_test, 4 | }; 5 | use hyper::{body::to_bytes, StatusCode}; 6 | 7 | viceroy_test!(json_dictionary_lookup_works, |is_component| { 8 | const FASTLY_TOML: &str = r#" 9 | name = "json-dictionary-lookup" 10 | description = "json dictionary lookup test" 11 | authors = ["Jill Bryson ", "Rose McDowall "] 12 | language = "rust" 13 | [local_server] 14 | [local_server.dictionaries] 15 | [local_server.dictionaries.animals] 16 | file = "../test-fixtures/data/json-dictionary.json" 17 | format = "json" 18 | "#; 19 | 20 | let resp = Test::using_fixture("dictionary-lookup.wasm") 21 | .adapt_component(is_component) 22 | .using_fastly_toml(FASTLY_TOML)? 23 | .against_empty() 24 | .await?; 25 | 26 | assert_eq!(resp.status(), StatusCode::OK); 27 | assert!(to_bytes(resp.into_body()) 28 | .await 29 | .expect("can read body") 30 | .to_vec() 31 | .is_empty()); 32 | 33 | Ok(()) 34 | }); 35 | 36 | viceroy_test!(inline_toml_dictionary_lookup_works, |is_component| { 37 | const FASTLY_TOML: &str = r#" 38 | name = "inline-toml-dictionary-lookup" 39 | description = "inline toml dictionary lookup test" 40 | authors = ["Jill Bryson ", "Rose McDowall "] 41 | language = "rust" 42 | [local_server] 43 | [local_server.dictionaries] 44 | [local_server.dictionaries.animals] 45 | format = "inline-toml" 46 | [local_server.dictionaries.animals.contents] 47 | dog = "woof" 48 | cat = "meow" 49 | "#; 50 | 51 | let resp = Test::using_fixture("dictionary-lookup.wasm") 52 | .adapt_component(is_component) 53 | .using_fastly_toml(FASTLY_TOML)? 54 | .against_empty() 55 | .await?; 56 | 57 | assert_eq!(resp.status(), StatusCode::OK); 58 | assert!(to_bytes(resp.into_body()) 59 | .await 60 | .expect("can read body") 61 | .to_vec() 62 | .is_empty()); 63 | 64 | Ok(()) 65 | }); 66 | 67 | viceroy_test!(missing_dictionary_works, |is_component| { 68 | const FASTLY_TOML: &str = r#" 69 | name = "missing-dictionary-config" 70 | description = "missing dictionary test" 71 | language = "rust" 72 | "#; 73 | 74 | let resp = Test::using_fixture("dictionary-lookup.wasm") 75 | .adapt_component(is_component) 76 | .using_fastly_toml(FASTLY_TOML)? 77 | .against_empty() 78 | .await?; 79 | 80 | assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); 81 | 82 | Ok(()) 83 | }); 84 | -------------------------------------------------------------------------------- /cli/tests/integration/downstream_req.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | common::{Test, TestResult}, 4 | viceroy_test, 5 | }, 6 | hyper::{Request, StatusCode}, 7 | }; 8 | 9 | viceroy_test!(downstream_request_works, |is_component| { 10 | let req = Request::get("/") 11 | .header("Accept", "text/html") 12 | .header("X-Custom-Test", "abcdef") 13 | .body("Hello, world!")?; 14 | let resp = Test::using_fixture("downstream-req.wasm") 15 | .adapt_component(is_component) 16 | .against(req) 17 | .await?; 18 | 19 | assert_eq!(resp.status(), StatusCode::OK); 20 | Ok(()) 21 | }); 22 | -------------------------------------------------------------------------------- /cli/tests/integration/edge_rate_limiting.rs: -------------------------------------------------------------------------------- 1 | //! Tests related to HTTP request and response bodies. 2 | 3 | use { 4 | crate::{ 5 | common::{Test, TestResult}, 6 | viceroy_test, 7 | }, 8 | hyper::StatusCode, 9 | }; 10 | 11 | viceroy_test!(check_hostcalls_implemented, |is_component| { 12 | let resp = Test::using_fixture("edge-rate-limiting.wasm") 13 | .adapt_component(is_component) 14 | .against_empty() 15 | .await?; 16 | assert_eq!(resp.status(), StatusCode::OK); 17 | Ok(()) 18 | }); 19 | -------------------------------------------------------------------------------- /cli/tests/integration/env_vars.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | common::{Test, TestResult}, 3 | viceroy_test, 4 | }; 5 | 6 | viceroy_test!(env_vars_are_set, |is_component| { 7 | let resp = Test::using_fixture("env-vars.wasm") 8 | .adapt_component(is_component) 9 | .against_empty() 10 | .await?; 11 | assert!(resp.status().is_success()); 12 | Ok(()) 13 | }); 14 | -------------------------------------------------------------------------------- /cli/tests/integration/grpc.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | common::{Test, TestResult}, 3 | viceroy_test, 4 | }; 5 | use hyper::http::response; 6 | use hyper::server::conn::AddrIncoming; 7 | use hyper::service::{make_service_fn, service_fn}; 8 | use hyper::{Request, Server, StatusCode}; 9 | use std::net::SocketAddr; 10 | 11 | viceroy_test!(grpc_creates_h2_connection, |is_component| { 12 | let test = Test::using_fixture("grpc.wasm").adapt_component(is_component); 13 | let server_addr: SocketAddr = "127.0.0.1:0".parse().expect("localhost parses"); 14 | let incoming = AddrIncoming::bind(&server_addr).expect("bind"); 15 | let bound_port = incoming.local_addr().port(); 16 | 17 | let service = make_service_fn(|_| async move { 18 | Ok::<_, std::io::Error>(service_fn(move |_req| async { 19 | response::Builder::new() 20 | .status(200) 21 | .body("Hello!".to_string()) 22 | })) 23 | }); 24 | 25 | let server = Server::builder(incoming).http2_only(true).serve(service); 26 | tokio::spawn(server); 27 | 28 | let resp = test 29 | .against( 30 | Request::post("/") 31 | .header("port", bound_port) 32 | .body("Hello, Viceroy!") 33 | .unwrap(), 34 | ) 35 | .await?; 36 | assert_eq!(resp.status(), StatusCode::OK); 37 | // The test below is not critical -- we've proved our point by returning 200 -- but seems 38 | // to trigger an error in Windows; it looks like there's a funny interaction between reading 39 | // the body and the stream having been closed, and we get a NO_ERROR error. So I've commented 40 | // it out, until there's a clear Hyper solution. 41 | // assert_eq!(resp.into_body().read_into_string().await?, "Hello!"); 42 | 43 | Ok(()) 44 | }); 45 | -------------------------------------------------------------------------------- /cli/tests/integration/hello_world.gzip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastly/Viceroy/2b7227e19220ed072c525be80e7c4406ece5aba9/cli/tests/integration/hello_world.gzip -------------------------------------------------------------------------------- /cli/tests/integration/http_semantics.rs: -------------------------------------------------------------------------------- 1 | //! Tests related to HTTP semantics (e.g. framing headers, status codes). 2 | 3 | use { 4 | crate::{ 5 | common::{Test, TestResult}, 6 | viceroy_test, 7 | }, 8 | hyper::{header, Request, Response, StatusCode}, 9 | }; 10 | 11 | viceroy_test!(framing_headers_are_overridden, |is_component| { 12 | // Set up the test harness 13 | let test = Test::using_fixture("bad-framing-headers.wasm") 14 | .adapt_component(is_component) 15 | // The "TheOrigin" backend checks framing headers on the request and then echos its body. 16 | .backend("TheOrigin", "/", None, |req| { 17 | assert!(!req.headers().contains_key(header::TRANSFER_ENCODING)); 18 | assert_eq!( 19 | req.headers().get(header::CONTENT_LENGTH), 20 | Some(&hyper::header::HeaderValue::from(9)) 21 | ); 22 | Response::new(Vec::from(&b"salutations"[..])) 23 | }) 24 | .await; 25 | 26 | let resp = test 27 | .via_hyper() 28 | .against(Request::post("/").body("greetings").unwrap()) 29 | .await?; 30 | 31 | assert_eq!(resp.status(), StatusCode::OK); 32 | 33 | assert!(!resp.headers().contains_key(header::TRANSFER_ENCODING)); 34 | assert_eq!( 35 | resp.headers().get(header::CONTENT_LENGTH), 36 | Some(&hyper::header::HeaderValue::from(11)) 37 | ); 38 | 39 | Ok(()) 40 | }); 41 | 42 | viceroy_test!(content_length_is_computed_correctly, |is_component| { 43 | // Set up the test harness 44 | let test = Test::using_fixture("content-length.wasm") 45 | .adapt_component(is_component) 46 | // The "TheOrigin" backend supplies a fixed-size body. 47 | .backend("TheOrigin", "/", None, |_| { 48 | Response::new(Vec::from(&b"ABCDEFGHIJKLMNOPQRST"[..])) 49 | }) 50 | .await; 51 | 52 | let resp = test.via_hyper().against_empty().await?; 53 | 54 | assert_eq!(resp.status(), StatusCode::OK); 55 | 56 | assert!(!resp.headers().contains_key(header::TRANSFER_ENCODING)); 57 | assert_eq!( 58 | resp.headers().get(header::CONTENT_LENGTH), 59 | Some(&hyper::header::HeaderValue::from(28)) 60 | ); 61 | let resp_body = resp.into_body().read_into_string().await.unwrap(); 62 | assert_eq!(resp_body, "ABCD12345xyzEFGHIJKLMNOPQRST"); 63 | 64 | Ok(()) 65 | }); 66 | -------------------------------------------------------------------------------- /cli/tests/integration/inspect.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | common::{Test, TestResult}, 4 | viceroy_test, 5 | }, 6 | hyper::{Request, StatusCode}, 7 | }; 8 | 9 | viceroy_test!(upstream_sync, |is_component| { 10 | // Set up the test harness: 11 | let test = Test::using_fixture("inspect.wasm").adapt_component(is_component); 12 | 13 | // And send a request to exercise the hostcall: 14 | let resp = test 15 | .against(Request::post("/").body("Hello, Viceroy!").unwrap()) 16 | .await?; 17 | 18 | assert_eq!(resp.status(), StatusCode::OK); 19 | assert_eq!( 20 | resp.into_body().read_into_string().await?, 21 | "inspect result: waf_response=200, tags=[], decision_ms=0ms, verdict=Allow" 22 | ); 23 | 24 | Ok(()) 25 | }); 26 | -------------------------------------------------------------------------------- /cli/tests/integration/logging.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | common::{Test, TestResult}, 4 | viceroy_test, 5 | }, 6 | hyper::StatusCode, 7 | std::{ 8 | io::{self, Write}, 9 | sync::{Arc, Mutex}, 10 | }, 11 | }; 12 | 13 | struct LogWriter(Vec>); 14 | 15 | impl Write for LogWriter { 16 | fn write(&mut self, buf: &[u8]) -> io::Result { 17 | self.0.push(buf.to_owned()); 18 | Ok(buf.len()) 19 | } 20 | 21 | fn flush(&mut self) -> io::Result<()> { 22 | Ok(()) 23 | } 24 | } 25 | 26 | viceroy_test!(logging_works, |is_component| { 27 | let log_writer = Arc::new(Mutex::new(LogWriter(Vec::new()))); 28 | let resp = Test::using_fixture("logging.wasm") 29 | .adapt_component(is_component) 30 | .capture_logs(log_writer.clone()) 31 | .log_stderr() 32 | .log_stdout() 33 | .against_empty() 34 | .await?; 35 | 36 | assert_eq!(resp.status(), StatusCode::OK); 37 | 38 | let mut logs = std::mem::take(&mut log_writer.lock().unwrap().0).into_iter(); 39 | let mut read_log_line = || String::from_utf8(logs.next().unwrap()).unwrap(); 40 | 41 | assert_eq!(read_log_line(), "inigo :: Who are you?\n"); 42 | assert_eq!(read_log_line(), "mib :: No one of consequence.\n"); 43 | assert_eq!(read_log_line(), "inigo :: I must know.\n"); 44 | assert_eq!(read_log_line(), "mib :: Get used to disappointment.\n"); 45 | 46 | assert_eq!( 47 | read_log_line(), 48 | "mib :: There is something\\nI ought to tell you.\n" 49 | ); 50 | assert_eq!(read_log_line(), "inigo :: Tell me.\\n\n"); 51 | assert_eq!(read_log_line(), "mib :: I'm not left-handed either.\n"); 52 | assert_eq!(read_log_line(), "inigo :: O_O\n"); 53 | 54 | assert_eq!(read_log_line(), "stdout :: logging from stdout\n"); 55 | assert_eq!(read_log_line(), "stderr :: logging from stderr\n"); 56 | 57 | // showcase line buffering on stdout 58 | assert_eq!(read_log_line(), "stdout :: log line terminates on flush\n"); 59 | assert_eq!(read_log_line(), "stdout :: newline completes a log line\n"); 60 | 61 | // showcase no buffering on stderr 62 | assert_eq!(read_log_line(), "stderr :: log line terminates on flush\n"); 63 | assert_eq!(read_log_line(), "stderr :: log line terminates\n"); 64 | assert_eq!(read_log_line(), "stderr :: on each write\n"); 65 | 66 | Ok(()) 67 | }); 68 | -------------------------------------------------------------------------------- /cli/tests/integration/main.rs: -------------------------------------------------------------------------------- 1 | mod acl; 2 | mod args; 3 | mod async_io; 4 | mod body; 5 | mod cache; 6 | mod client_certs; 7 | mod common; 8 | mod config_store_lookup; 9 | mod device_detection_lookup; 10 | mod dictionary_lookup; 11 | mod downstream_req; 12 | mod edge_rate_limiting; 13 | mod env_vars; 14 | mod geolocation_lookup; 15 | mod grpc; 16 | mod http_semantics; 17 | mod inspect; 18 | mod kv_store; 19 | mod logging; 20 | mod memory; 21 | mod request; 22 | mod response; 23 | mod secret_store; 24 | mod sending_response; 25 | mod shielding; 26 | mod sleep; 27 | mod unknown_import_behavior; 28 | mod upstream; 29 | mod upstream_async; 30 | mod upstream_dynamic; 31 | mod upstream_streaming; 32 | mod vcpu_time; 33 | -------------------------------------------------------------------------------- /cli/tests/integration/memory.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | common::{Test, TestResult}, 3 | viceroy_test, 4 | }; 5 | use hyper::body::to_bytes; 6 | use hyper::{Request, StatusCode}; 7 | 8 | viceroy_test!(direct_wasm_works, |is_component| { 9 | let resp = Test::using_wat_fixture("return_ok.wat") 10 | .adapt_component(is_component) 11 | .against_empty() 12 | .await?; 13 | assert_eq!(resp.status(), StatusCode::OK); 14 | Ok(()) 15 | }); 16 | 17 | viceroy_test!(heap_limit_test_ok, |is_component| { 18 | let resp = Test::using_wat_fixture("combined_heap_limits.wat") 19 | .adapt_component(is_component) 20 | .against( 21 | Request::get("/") 22 | .header("guest-kb", "235") 23 | .header("header-kb", "1") 24 | .header("body-kb", "16") 25 | .body("") 26 | .unwrap(), 27 | ) 28 | .await?; 29 | println!("response: {:?}", resp); 30 | assert_eq!(resp.status(), StatusCode::OK); 31 | assert_eq!(resp.headers().len(), 16); 32 | assert!(resp.headers().contains_key("x-test-header-3")); 33 | assert_eq!( 34 | resp.headers().get("x-test-header-12").unwrap(), 35 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit viverra." 36 | ); 37 | let body = resp.into_body(); 38 | assert_eq!(to_bytes(body).await.unwrap().len(), 16 * 1024); 39 | Ok(()) 40 | }); 41 | 42 | viceroy_test!(heap_limit_test_bad, |is_component| { 43 | let resp = Test::using_wat_fixture("combined_heap_limits.wat") 44 | .adapt_component(is_component) 45 | .against( 46 | Request::get("/") 47 | .header("guest-kb", "150000") 48 | .body("") 49 | .unwrap(), 50 | ) 51 | .await?; 52 | assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); 53 | Ok(()) 54 | }); 55 | -------------------------------------------------------------------------------- /cli/tests/integration/request.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | common::{Test, TestResult}, 4 | viceroy_test, 5 | }, 6 | hyper::StatusCode, 7 | }; 8 | 9 | viceroy_test!(request_works, |is_component| { 10 | let resp = Test::using_fixture("request.wasm") 11 | .adapt_component(is_component) 12 | .against_empty() 13 | .await?; 14 | assert_eq!(resp.status(), StatusCode::OK); 15 | Ok(()) 16 | }); 17 | -------------------------------------------------------------------------------- /cli/tests/integration/response.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | common::{Test, TestResult}, 4 | viceroy_test, 5 | }, 6 | hyper::StatusCode, 7 | }; 8 | 9 | viceroy_test!(response_works, |is_component| { 10 | let resp = Test::using_fixture("response.wasm") 11 | .adapt_component(is_component) 12 | .against_empty() 13 | .await?; 14 | assert_eq!(resp.status(), StatusCode::OK); 15 | Ok(()) 16 | }); 17 | -------------------------------------------------------------------------------- /cli/tests/integration/sleep.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | common::{Test, TestResult}, 3 | viceroy_test, 4 | }; 5 | use hyper::{body::to_bytes, StatusCode}; 6 | 7 | // Run a program that only sleeps. This exercises async functionality in wasi. 8 | // Check that an empty response is sent downstream by default. 9 | // 10 | // `sleep.wasm` is a guest program which sleeps for 100 milliseconds,then returns. 11 | viceroy_test!(empty_ok_response_by_default_after_sleep, |is_component| { 12 | let resp = Test::using_fixture("sleep.wasm") 13 | .adapt_component(is_component) 14 | .against_empty() 15 | .await?; 16 | 17 | assert_eq!(resp.status(), StatusCode::OK); 18 | assert!(to_bytes(resp.into_body()) 19 | .await 20 | .expect("can read body") 21 | .to_vec() 22 | .is_empty()); 23 | 24 | Ok(()) 25 | }); 26 | -------------------------------------------------------------------------------- /cli/tests/integration/unknown_import_behavior.rs: -------------------------------------------------------------------------------- 1 | use hyper::{Request, Response, StatusCode}; 2 | use viceroy_lib::config::UnknownImportBehavior; 3 | 4 | use crate::common::{Test, TestResult}; 5 | 6 | /// A test using the default behavior, where the unknown import will fail to link. 7 | #[tokio::test(flavor = "multi_thread")] 8 | async fn default_behavior_link_failure() -> TestResult { 9 | let res = Test::using_fixture("unknown-import.wasm") 10 | .against_empty() 11 | .await; 12 | 13 | let err = res.expect_err("should be a link failure"); 14 | assert!(err 15 | .to_string() 16 | .contains("unknown import: `unknown_module::unknown_function` has not been defined")); 17 | 18 | Ok(()) 19 | } 20 | 21 | /// A test using the trap behavior, where calling the unknown import will cause a runtime trap. 22 | #[tokio::test(flavor = "multi_thread")] 23 | async fn trap_behavior_function_called() -> TestResult { 24 | let resp = Test::using_fixture("unknown-import.wasm") 25 | .using_unknown_import_behavior(UnknownImportBehavior::Trap) 26 | .against(Request::get("/").header("call-it", "yes").body("").unwrap()) 27 | .await?; 28 | 29 | assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); 30 | let body_bytes = hyper::body::to_bytes(resp.into_body()).await?; 31 | let body = std::str::from_utf8(&body_bytes)?; 32 | // The backtrace contains things like stack addresses and gensyms, so we just look for a couple 33 | // key parts that should be relatively stable across invocations and wasmtime 34 | // versions. Fundamentally though, we're still making assertions based on pretty-printed errors, 35 | // so beware of trivial breakages. 36 | assert!(body.contains("error while executing at wasm backtrace")); 37 | assert!(body.contains("unknown_import::main::")); 38 | 39 | Ok(()) 40 | } 41 | 42 | /// A test using the trap behavior, where not calling the function means execution proceeds normally. 43 | #[tokio::test(flavor = "multi_thread")] 44 | async fn trap_behavior_function_not_called() -> TestResult { 45 | let resp = Test::using_fixture("unknown-import.wasm") 46 | .backend("TheOrigin", "/", None, |_req| { 47 | Response::builder() 48 | .status(StatusCode::OK) 49 | .body(vec![]) 50 | .unwrap() 51 | }) 52 | .await 53 | .using_unknown_import_behavior(UnknownImportBehavior::Trap) 54 | .against_empty() 55 | .await?; 56 | 57 | assert_eq!(resp.status(), StatusCode::OK); 58 | 59 | Ok(()) 60 | } 61 | 62 | /// A test using the zero-or-null value behavior, where calling the function returns an expected 63 | /// zero value and execution proceeds normally. 64 | #[tokio::test(flavor = "multi_thread")] 65 | async fn zero_or_null_behavior_function_called() -> TestResult { 66 | let resp = Test::using_fixture("unknown-import.wasm") 67 | .backend("TheOrigin", "/", None, |_req| { 68 | Response::builder() 69 | .status(StatusCode::OK) 70 | .body(vec![]) 71 | .unwrap() 72 | }) 73 | .await 74 | .using_unknown_import_behavior(UnknownImportBehavior::ZeroOrNull) 75 | .against(Request::get("/").header("call-it", "yes").body("").unwrap()) 76 | .await?; 77 | 78 | assert_eq!(resp.status(), StatusCode::OK); 79 | 80 | Ok(()) 81 | } 82 | -------------------------------------------------------------------------------- /cli/tests/integration/upstream_async.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use tokio::sync::Semaphore; 4 | 5 | use { 6 | crate::{ 7 | common::{Test, TestResult}, 8 | viceroy_test, 9 | }, 10 | hyper::{Response, StatusCode}, 11 | }; 12 | 13 | viceroy_test!(upstream_async_methods, |is_component| { 14 | // Set up two backends that share a semaphore that starts with zero permits. `backend1` must 15 | // take a semaphore permit and then "forget" it before returning its response. `backend2` adds a 16 | // permit to the semaphore and promptly returns. This relationship allows the test fixtures to 17 | // examine the behavior of the various pending request operators beyond just whether they 18 | // eventually return the expected response. 19 | let sema_backend1 = Arc::new(Semaphore::new(0)); 20 | let sema_backend2 = sema_backend1.clone(); 21 | let test = Test::using_fixture("upstream-async.wasm") 22 | .adapt_component(is_component) 23 | .async_backend("backend1", "/", None, move |_| { 24 | let sema_backend1 = sema_backend1.clone(); 25 | Box::new(async move { 26 | sema_backend1.acquire().await.unwrap().forget(); 27 | Response::builder() 28 | .header("Backend-1-Response", "") 29 | .status(StatusCode::OK) 30 | .body(hyper::Body::empty()) 31 | .unwrap() 32 | }) 33 | }) 34 | .await 35 | .async_backend("backend2", "/", None, move |_| { 36 | let sema_backend2 = sema_backend2.clone(); 37 | Box::new(async move { 38 | sema_backend2.add_permits(1); 39 | Response::builder() 40 | .header("Backend-2-Response", "") 41 | .status(StatusCode::OK) 42 | .body(hyper::Body::empty()) 43 | .unwrap() 44 | }) 45 | }) 46 | .await; 47 | 48 | // The meat of the test is on the guest side; we just check that we made it through successfully 49 | let resp = test.against_empty().await?; 50 | assert_eq!(resp.status(), StatusCode::OK); 51 | Ok(()) 52 | }); 53 | -------------------------------------------------------------------------------- /cli/tests/integration/upstream_streaming.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | common::{Test, TestResult}, 4 | viceroy_test, 5 | }, 6 | hyper::{body::HttpBody, Response, StatusCode}, 7 | }; 8 | 9 | // Test that guests can stream a body into an upstream request. 10 | viceroy_test!(upstream_streaming, |is_component| { 11 | // Set up the test harness 12 | let test = Test::using_fixture("upstream-streaming.wasm") 13 | .adapt_component(is_component) 14 | // The "origin" backend simply echos the request body 15 | .backend("origin", "/", None, |req| Response::new(req.into_body())) 16 | .await; 17 | 18 | // Test with an empty request 19 | let mut resp = test.against_empty().await?; 20 | assert_eq!(resp.status(), StatusCode::OK); 21 | 22 | // accumulate the entire body to a vector 23 | let mut body = Vec::new(); 24 | while let Some(chunk) = resp.data().await { 25 | body.extend_from_slice(&chunk?); 26 | } 27 | 28 | // work with the body as a string, breaking it into lines 29 | let body_str = String::from_utf8(body).unwrap(); 30 | let mut i: u32 = 0; 31 | for line in body_str.lines() { 32 | assert_eq!(line, i.to_string()); 33 | i += 1; 34 | } 35 | assert_eq!(i, 1000); 36 | 37 | Ok(()) 38 | }); 39 | -------------------------------------------------------------------------------- /cli/tests/integration/vcpu_time.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | common::{Test, TestResult}, 3 | viceroy_test, 4 | }; 5 | use hyper::{Request, Response, StatusCode}; 6 | 7 | viceroy_test!(vcpu_time_getter_works, |is_component| { 8 | let req = Request::get("/") 9 | .header("Accept", "text/html") 10 | .body("Hello, world!") 11 | .unwrap(); 12 | 13 | let resp = Test::using_fixture("vcpu_time_test.wasm") 14 | .adapt_component(is_component) 15 | .backend("slow-server", "/", None, |_| { 16 | std::thread::sleep(std::time::Duration::from_millis(4000)); 17 | Response::builder() 18 | .status(StatusCode::OK) 19 | .body(vec![]) 20 | .unwrap() 21 | }) 22 | .await 23 | .against(req) 24 | .await?; 25 | 26 | assert_eq!(resp.status(), StatusCode::OK); 27 | Ok(()) 28 | }); 29 | -------------------------------------------------------------------------------- /cli/tests/trap-test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "trap-test" 3 | description = "A test to exercise the Viceroy functionality in which a hostcall experiences a FatalError and terminates the instance." 4 | version = "0.1.0" 5 | authors = [] 6 | edition = "2021" 7 | license = "Apache-2.0 WITH LLVM-exception" 8 | publish = false 9 | 10 | [dependencies] 11 | anyhow = "1.0.31" 12 | futures = "0.3.0" 13 | http = "0.2.1" 14 | hyper = "=0.14.26" 15 | tokio = { version = "1.2", features = ["full"] } 16 | tracing-subscriber = "0.2.19" 17 | viceroy-lib = { path = "../../../lib", features = ["test-fatalerror-config"] } 18 | 19 | # To indicate to cargo that this trap-test is not a member of the testing workspace specified by 20 | # ~/cli/tests/fixtures/Cargo.toml, place an empty workspace specification here. 21 | [workspace] 22 | -------------------------------------------------------------------------------- /cli/tests/trap-test/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use { 3 | hyper::{Body, Request, StatusCode}, 4 | viceroy_lib::{ExecuteCtx, ProfilingStrategy}, 5 | }; 6 | 7 | /// A shorthand for the path to our test fixtures' build artifacts for Rust tests. 8 | const RUST_FIXTURE_PATH: &str = "../../../test-fixtures/target/wasm32-wasip1/debug/"; 9 | 10 | /// A catch-all error, so we can easily use `?` in test cases. 11 | pub type Error = Box; 12 | 13 | /// Handy alias for the return type of async Tokio tests 14 | pub type TestResult = Result<(), Error>; 15 | 16 | async fn fatal_error_traps_impl(adapt_core_wasm: bool) -> TestResult { 17 | let module_path = format!("{RUST_FIXTURE_PATH}/response.wasm"); 18 | let ctx = ExecuteCtx::new( 19 | module_path, 20 | ProfilingStrategy::None, 21 | HashSet::new(), 22 | None, 23 | viceroy_lib::config::UnknownImportBehavior::LinkError, 24 | adapt_core_wasm, 25 | )?; 26 | let req = Request::get("http://127.0.0.1:7676/").body(Body::from(""))?; 27 | let local = "127.0.0.1:80".parse().unwrap(); 28 | let remote = "127.0.0.1:0".parse().unwrap(); 29 | let resp = ctx 30 | .handle_request_with_runtime_error(req, local, remote) 31 | .await?; 32 | 33 | // The Guest was terminated and so should return a 500. 34 | assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); 35 | 36 | // Examine the cause in the body and assert that it is the expected 37 | // Trap error supplied by the Guest. 38 | 39 | let body = resp.into_body().read_into_string().await?; 40 | 41 | assert_eq!( 42 | body, 43 | "Fatal error: [A fatal error occurred in the test-only implementation of header_values_get]" 44 | ); 45 | 46 | Ok(()) 47 | } 48 | 49 | #[tokio::test(flavor = "multi_thread")] 50 | async fn fatal_error_traps() -> TestResult { 51 | fatal_error_traps_impl(false).await 52 | } 53 | 54 | #[tokio::test(flavor = "multi_thread")] 55 | async fn fatal_error_traps_component() -> TestResult { 56 | fatal_error_traps_impl(true).await 57 | } 58 | -------------------------------------------------------------------------------- /cli/tests/wasm/exit_code_4.wat: -------------------------------------------------------------------------------- 1 | (module 2 | (import "wasi_snapshot_preview1" "proc_exit" 3 | (func $proc_exit (param i32))) 4 | (export "_start" (func $_start)) 5 | (memory 10) 6 | (export "memory" (memory 0)) 7 | 8 | (func $_start 9 | (call $proc_exit (i32.const 4)) 10 | ) 11 | ) -------------------------------------------------------------------------------- /cli/tests/wasm/invalid.wat: -------------------------------------------------------------------------------- 1 | This is not a valid wat file 2 | -------------------------------------------------------------------------------- /cli/tests/wasm/minimal.wasm: -------------------------------------------------------------------------------- 1 | asm -------------------------------------------------------------------------------- /cli/tests/wasm/minimal.wat: -------------------------------------------------------------------------------- 1 | (module) 2 | -------------------------------------------------------------------------------- /cli/tests/wasm/trapping.wat: -------------------------------------------------------------------------------- 1 | (module 2 | (export "_start" (func $_start)) 3 | (func $_start 4 | unreachable 5 | ) 6 | ) -------------------------------------------------------------------------------- /crates/adapter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "viceroy-component-adapter" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | wasi = { workspace = true } 9 | wit-bindgen-rust-macro = { workspace = true } 10 | byte-array-literals = { workspace = true } 11 | bitflags = { workspace = true } 12 | 13 | [build-dependencies] 14 | wasm-encoder = { workspace = true } 15 | object = { workspace = true } 16 | 17 | [lib] 18 | test = false 19 | crate-type = ["cdylib"] 20 | -------------------------------------------------------------------------------- /crates/adapter/README.md: -------------------------------------------------------------------------------- 1 | # The Fastly Component Adapter 2 | 3 | This crate builds a wasm module that adapts both the preview1 api and the fastly 4 | compute host calls to the component model. It started as a fork of the 5 | [wasi_snapshot_preview1] component adapter with the `proxy` feature manually 6 | expanded out, with all of the fastly-specific functionality mostly being added 7 | in the `src/fastly` tree. The exception to this is the reactor export defined 8 | by the compute world of `compute.wit`, whose definition is in `src/lib.rs` 9 | instead of being defined in `src/fastly`, as the `wit-bindgen::generate!` makes 10 | assumptions about relative module paths that make it hard to define elsewhere. 11 | 12 | Changes to the adapter require running the top-level `make adapter` target, and 13 | committing the resulting `lib/data/viceroy-component-adapter.wasm` wasm module. 14 | This is a bit unfortunate, but as there's no way to hook the packaging step with 15 | cargo, committing the binary is the easiest way to ensure that fresh checkouts 16 | of this repository and packaged versions of the crates both build seamlessly. 17 | 18 | ## Adding New Host Calls 19 | 20 | When adding new host calls, the adapter will need to be updated to know how they 21 | should be adapted to the component model. In most cases, this will involve 22 | updating the `/lib/wit/deps/fastly/compute.wit` package to describe what the 23 | component imports of the new host call should look like, implementing it in both 24 | `/lib/src/wiggle_abi` and `/lib/src/component`, and then finally adding a 25 | version of the host call to the adapter in `src/fastly`. As the adapter builds 26 | with the same `compute.wit` that `viceroy-lib` does, the imports will 27 | automatically be available through the top-level `bindings` module. 28 | 29 | [wasi_snapshot_preview1]: https://github.com/bytecodealliance/wasmtime/tree/main/crates/wasi-preview1-component-adapter 30 | -------------------------------------------------------------------------------- /crates/adapter/byte-array-literals/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "byte-array-literals" 3 | version = "0.0.0" 4 | publish = false 5 | 6 | [lib] 7 | proc-macro = true 8 | test = false 9 | doctest = false 10 | -------------------------------------------------------------------------------- /crates/adapter/byte-array-literals/README.md: -------------------------------------------------------------------------------- 1 | # byte-array-literals 2 | 3 | This crate exists to solve a very peculiar problem for the 4 | `wasi-preview1-component-adapter`: we want to use string literals in our 5 | source code, but the resulting binary (when compiled for 6 | wasm32-unknown-unknown) cannot contain any data sections. 7 | 8 | The answer that @sunfishcode discovered is that these string literals, if 9 | represented as an array of u8 literals, these will somehow not end up in the 10 | data section, at least when compiled with opt-level='s' on today's rustc 11 | (1.69.0). So, this crate exists to transform these literals using a proc 12 | macro. 13 | 14 | It is very possible this cheat code will abruptly stop working in some future 15 | compiler, but we'll cross that bridge when we get to it. 16 | -------------------------------------------------------------------------------- /crates/adapter/byte-array-literals/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use proc_macro::{Delimiter, Group, Literal, Punct, Spacing, TokenStream, TokenTree}; 4 | 5 | /// Expand a `str` literal into a byte array. 6 | #[proc_macro] 7 | pub fn str(input: TokenStream) -> TokenStream { 8 | let rv = convert_str(input); 9 | 10 | vec![TokenTree::Group(Group::new( 11 | Delimiter::Bracket, 12 | rv.into_iter().collect(), 13 | ))] 14 | .into_iter() 15 | .collect() 16 | } 17 | 18 | /// The same as `str` but appends a `'\n'`. 19 | #[proc_macro] 20 | pub fn str_nl(input: TokenStream) -> TokenStream { 21 | let mut rv = convert_str(input); 22 | 23 | rv.push(TokenTree::Literal(Literal::u8_suffixed(b'\n'))); 24 | 25 | vec![TokenTree::Group(Group::new( 26 | Delimiter::Bracket, 27 | rv.into_iter().collect(), 28 | ))] 29 | .into_iter() 30 | .collect() 31 | } 32 | 33 | fn convert_str(input: TokenStream) -> Vec { 34 | let mut it = input.into_iter(); 35 | 36 | let mut tokens = Vec::new(); 37 | match it.next() { 38 | Some(TokenTree::Literal(l)) => { 39 | for b in to_string(l).into_bytes() { 40 | tokens.push(TokenTree::Literal(Literal::u8_suffixed(b))); 41 | tokens.push(TokenTree::Punct(Punct::new(',', Spacing::Alone))); 42 | } 43 | } 44 | _ => panic!(), 45 | } 46 | 47 | assert!(it.next().is_none()); 48 | tokens 49 | } 50 | 51 | fn to_string(lit: Literal) -> String { 52 | let formatted = lit.to_string(); 53 | 54 | let mut it = formatted.chars(); 55 | assert_eq!(it.next(), Some('"')); 56 | 57 | let mut rv = String::new(); 58 | loop { 59 | match it.next() { 60 | Some('"') => match it.next() { 61 | Some(_) => panic!(), 62 | None => break, 63 | }, 64 | Some('\\') => match it.next() { 65 | Some('x') => { 66 | let hi = it.next().unwrap().to_digit(16).unwrap(); 67 | let lo = it.next().unwrap().to_digit(16).unwrap(); 68 | let v = (hi << 16) | lo; 69 | rv.push(v as u8 as char); 70 | } 71 | Some('u') => { 72 | assert_eq!(it.next(), Some('{')); 73 | let mut c = it.next().unwrap(); 74 | let mut ch = 0; 75 | while let Some(v) = c.to_digit(16) { 76 | ch *= 16; 77 | ch |= v; 78 | c = it.next().unwrap(); 79 | } 80 | assert_eq!(c, '}'); 81 | rv.push(::std::char::from_u32(ch).unwrap()); 82 | } 83 | Some('0') => rv.push('\0'), 84 | Some('\\') => rv.push('\\'), 85 | Some('\"') => rv.push('\"'), 86 | Some('r') => rv.push('\r'), 87 | Some('n') => rv.push('\n'), 88 | Some('t') => rv.push('\t'), 89 | Some(_) => panic!(), 90 | None => panic!(), 91 | }, 92 | Some(c) => rv.push(c), 93 | None => panic!(), 94 | } 95 | } 96 | 97 | rv 98 | } 99 | -------------------------------------------------------------------------------- /crates/adapter/src/fastly/config_store.rs: -------------------------------------------------------------------------------- 1 | use super::FastlyStatus; 2 | use crate::{alloc_result_opt, bindings::fastly::api::config_store, TrappingUnwrap}; 3 | use core::slice; 4 | 5 | pub type ConfigStoreHandle = u32; 6 | 7 | #[export_name = "fastly_config_store#open"] 8 | pub fn open( 9 | name: *const u8, 10 | name_len: usize, 11 | store_handle_out: *mut ConfigStoreHandle, 12 | ) -> FastlyStatus { 13 | let name = unsafe { slice::from_raw_parts(name, name_len) }; 14 | match config_store::open(name) { 15 | Ok(res) => { 16 | unsafe { 17 | *store_handle_out = res; 18 | } 19 | FastlyStatus::OK 20 | } 21 | Err(e) => e.into(), 22 | } 23 | } 24 | 25 | #[export_name = "fastly_config_store#get"] 26 | pub fn get( 27 | store_handle: ConfigStoreHandle, 28 | key: *const u8, 29 | key_len: usize, 30 | value: *mut u8, 31 | value_max_len: usize, 32 | nwritten: *mut usize, 33 | ) -> FastlyStatus { 34 | let key = unsafe { slice::from_raw_parts(key, key_len) }; 35 | alloc_result_opt!(value, value_max_len, nwritten, { 36 | config_store::get( 37 | store_handle, 38 | key, 39 | u64::try_from(value_max_len).trapping_unwrap(), 40 | ) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /crates/adapter/src/fastly/error.rs: -------------------------------------------------------------------------------- 1 | use crate::StateError; 2 | 3 | #[derive(PartialEq, Eq)] 4 | #[repr(C)] 5 | pub struct FastlyStatus(i32); 6 | 7 | impl StateError for FastlyStatus { 8 | const SUCCESS: Self = Self::OK; 9 | } 10 | 11 | impl FastlyStatus { 12 | pub const OK: Self = FastlyStatus(0); 13 | pub const UNKNOWN_ERROR: Self = FastlyStatus(1); 14 | pub const INVALID_ARGUMENT: Self = Self(2); 15 | pub const UNSUPPORTED: Self = Self(5); 16 | pub const NONE: Self = FastlyStatus(10); 17 | } 18 | 19 | impl From for FastlyStatus { 20 | fn from(err: crate::bindings::fastly::api::types::Error) -> Self { 21 | use crate::bindings::fastly::api::types::Error; 22 | FastlyStatus(match err { 23 | // use black_box here to prevent rustc/llvm from generating a switch table 24 | Error::UnknownError => std::hint::black_box(100), 25 | Error::GenericError => 1, 26 | Error::InvalidArgument => Self::INVALID_ARGUMENT.0, 27 | Error::BadHandle => 3, 28 | Error::BufferLen(_) => 4, 29 | Error::Unsupported => 5, 30 | Error::BadAlign => 6, 31 | Error::HttpInvalid => 7, 32 | Error::HttpUser => 8, 33 | Error::HttpIncomplete => 9, 34 | Error::OptionalNone => 10, 35 | Error::HttpHeadTooLarge => 11, 36 | Error::HttpInvalidStatus => 12, 37 | Error::LimitExceeded => 13, 38 | }) 39 | } 40 | } 41 | 42 | pub(crate) fn convert_result( 43 | res: Result<(), crate::bindings::fastly::api::types::Error>, 44 | ) -> FastlyStatus { 45 | match res { 46 | Ok(()) => FastlyStatus::OK, 47 | Err(e) => FastlyStatus::from(e), 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /crates/adapter/src/fastly/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! with_buffer { 3 | ($buf:expr, $len:expr, $alloc:block, |$res:ident| $free:block) => { 4 | crate::State::with::(|state| { 5 | let $res = state.with_one_import_alloc($buf, $len, || $alloc); 6 | $free; 7 | Ok(()) 8 | }) 9 | }; 10 | } 11 | 12 | #[macro_export] 13 | macro_rules! alloc_result { 14 | ($buf:expr, $len:expr, $nwritten:expr, $block:block) => { 15 | crate::with_buffer!($buf, $len, $block, |res| { 16 | let res = crate::handle_buffer_len!(res, $nwritten); 17 | unsafe { 18 | *$nwritten = res.len(); 19 | } 20 | 21 | std::mem::forget(res); 22 | }) 23 | }; 24 | } 25 | 26 | #[macro_export] 27 | macro_rules! alloc_result_opt { 28 | ($buf:expr, $len:expr, $nwritten:expr, $block:block) => { 29 | crate::with_buffer!($buf, $len, $block, |res| { 30 | let res = crate::handle_buffer_len!(res, $nwritten).ok_or(FastlyStatus::NONE)?; 31 | unsafe { 32 | *$nwritten = res.len(); 33 | } 34 | 35 | std::mem::forget(res); 36 | }) 37 | }; 38 | } 39 | 40 | #[macro_export] 41 | macro_rules! handle_buffer_len { 42 | ($res:ident, $nwritten:expr) => { 43 | match $res { 44 | Ok(res) => res, 45 | Err(err) => { 46 | if let crate::bindings::fastly::api::types::Error::BufferLen(needed) = err { 47 | unsafe { 48 | *$nwritten = 49 | crate::TrappingUnwrap::trapping_unwrap(usize::try_from(needed)); 50 | } 51 | } 52 | 53 | return Err(err.into()); 54 | } 55 | } 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /crates/adapter/src/fastly/mod.rs: -------------------------------------------------------------------------------- 1 | mod cache; 2 | mod config_store; 3 | mod core; 4 | mod error; 5 | mod http_cache; 6 | mod macros; 7 | 8 | pub(crate) use error::*; 9 | 10 | pub use cache::*; 11 | pub use config_store::*; 12 | pub use core::*; 13 | pub use http_cache::*; 14 | -------------------------------------------------------------------------------- /crates/adapter/src/macros.rs: -------------------------------------------------------------------------------- 1 | //! Minimal versions of standard-library panicking and printing macros. 2 | //! 3 | //! We're avoiding static initializers, so we can't have things like string 4 | //! literals. Replace the standard assert macros with simpler implementations. 5 | 6 | use crate::bindings::wasi::cli::stderr::get_stderr; 7 | 8 | #[allow(dead_code)] 9 | #[doc(hidden)] 10 | pub fn print(message: &[u8]) { 11 | let _ = get_stderr().blocking_write_and_flush(message); 12 | } 13 | 14 | /// A minimal `eprint` for debugging. 15 | #[allow(unused_macros)] 16 | macro_rules! eprint { 17 | ($arg:tt) => {{ 18 | // We have to expand string literals into byte arrays to prevent them 19 | // from getting statically initialized. 20 | let message = byte_array_literals::str!($arg); 21 | $crate::macros::print(&message); 22 | }}; 23 | } 24 | 25 | /// A minimal `eprintln` for debugging. 26 | #[allow(unused_macros)] 27 | macro_rules! eprintln { 28 | ($arg:tt) => {{ 29 | // We have to expand string literals into byte arrays to prevent them 30 | // from getting statically initialized. 31 | let message = byte_array_literals::str_nl!($arg); 32 | $crate::macros::print(&message); 33 | }}; 34 | } 35 | 36 | pub(crate) fn eprint_u32(x: u32) { 37 | if x == 0 { 38 | eprint!("0"); 39 | } else { 40 | eprint_u32_impl(x) 41 | } 42 | 43 | fn eprint_u32_impl(x: u32) { 44 | if x != 0 { 45 | eprint_u32_impl(x / 10); 46 | 47 | let digit = [b'0' + ((x % 10) as u8)]; 48 | crate::macros::print(&digit); 49 | } 50 | } 51 | } 52 | 53 | /// A minimal `unreachable`. 54 | macro_rules! unreachable { 55 | () => {{ 56 | eprint!("unreachable executed at adapter line "); 57 | crate::macros::eprint_u32(line!()); 58 | eprint!("\n"); 59 | #[cfg(target_arch = "wasm32")] 60 | core::arch::wasm32::unreachable(); 61 | // This is here to keep rust-analyzer happy when building for native: 62 | #[cfg(not(target_arch = "wasm32"))] 63 | std::process::abort(); 64 | }}; 65 | 66 | ($arg:tt) => {{ 67 | eprint!("unreachable executed at adapter line "); 68 | crate::macros::eprint_u32(line!()); 69 | eprint!(": "); 70 | eprintln!($arg); 71 | eprint!("\n"); 72 | #[cfg(target_arch = "wasm32")] 73 | core::arch::wasm32::unreachable(); 74 | // This is here to keep rust-analyzer happy when building for native: 75 | #[cfg(not(target_arch = "wasm32"))] 76 | std::process::abort(); 77 | }}; 78 | } 79 | 80 | /// A minimal `assert`. 81 | macro_rules! assert { 82 | ($cond:expr $(,)?) => { 83 | if !$cond { 84 | unreachable!("assertion failed") 85 | } 86 | }; 87 | } 88 | 89 | /// A minimal `assert_eq`. 90 | macro_rules! assert_eq { 91 | ($left:expr, $right:expr $(,)?) => { 92 | assert!($left == $right); 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /doc/RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing Viceroy 2 | 3 | Below are the steps needed to do a Viceroy release: 4 | 5 | 1. Make sure the Viceroy version has been bumped up to the current release 6 | version. You might need to bump the minor version (e.g. 0.2.0 to 0.3.0) if 7 | there are any semver breaking changes. Review the changes since the last 8 | release just to be sure. 9 | 1. Update the `Cargo.lock` files by running `make generate-lockfile`. 10 | 1. Update `CHANGELOG.md` so that it contains all of the updates since the 11 | previous version as its own commit. Remove the "## Unreleased" header. 12 | 1. Create a local branch in the form `release-x.y.z` where `x`, `y`, and `z` are 13 | the major, minor, and patch versions of Viceroy and have the tip of the 14 | branch contain the Changelog commit. 15 | 1. Run `make ci` locally to make sure that everything will pass before pushing 16 | the branch and opening up a PR. 17 | 1. After you get approval, run `git tag vx.y.z HEAD && git push origin --tags`. 18 | Pushing this tag will kick off a build for all of the release artifacts. 19 | 1. After CI completes, we should publish each crate in the workspace to the 20 | crates.io registry. Note that we must do this in order of dependencies. So, 21 | 1. `(cd lib && cargo publish)` 22 | 1. `(cd cli && cargo publish)` 23 | 1. Now, we should return to our release PR. 24 | 1. Update the version fields in `lib/Cargo.toml` and `cli/Cargo.toml` to the 25 | next patch version (so `z + 1`). 26 | 1. Update the dependency on `viceroy-lib` in `cli/Cargo.toml` to the next 27 | patch version. 28 | 1. Update all the lockfiles by running `make generate-lockfile`. 29 | 1. Restore the `## Unreleased` header at the top of `CHANGELOG.md`. 30 | 1. Get another approval and do a merge commit (not a squash) when CI passes. We don't squash because we want the tagged commit to be contained within the `main` branch 31 | -------------------------------------------------------------------------------- /doc/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastly/Viceroy/2b7227e19220ed072c525be80e7c4406ece5aba9/doc/logo.png -------------------------------------------------------------------------------- /lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "viceroy-lib" 3 | version = "0.13.1" 4 | description = "Viceroy implementation details." 5 | authors = ["Fastly"] 6 | edition = "2021" 7 | license = "Apache-2.0 WITH LLVM-exception" 8 | documentation = "https://docs.rs/viceroy-lib" 9 | homepage = "https://github.com/fastly/Viceroy" 10 | repository = "https://github.com/fastly/Viceroy" 11 | keywords = ["wasm", "fastly"] 12 | categories = [ 13 | "development-tools", 14 | "network-programming", 15 | "simulation", 16 | "wasm" 17 | ] 18 | include = [ 19 | "../CHANGELOG.md", 20 | "../SECURITY.md", 21 | "src/**/*", 22 | "wit/**/*", 23 | "compute-at-edge-abi/**/*.witx", 24 | "data/*.wasm", 25 | ] 26 | # MSRV: 1.82 for is_none_or 27 | rust-version = "1.82" 28 | 29 | [dependencies] 30 | anyhow = { workspace = true } 31 | async-trait = "0.1.59" 32 | bytes = "^1.2.1" 33 | bytesize = "^1.1.0" 34 | cfg-if = "^1.0" 35 | clap = { workspace = true } 36 | cranelift-entity = "^0.88.1" 37 | fastly-shared = "^0.10.1" 38 | flate2 = "^1.0.24" 39 | futures = { workspace = true } 40 | http = "^0.2.8" 41 | http-body = "^0.4.5" 42 | hyper = { workspace = true } 43 | itertools = { workspace = true } 44 | lazy_static = "^1.4.0" 45 | pin-project = { workspace = true } 46 | regex = "^1.3.9" 47 | rustls = "^0.21.1" 48 | rustls-native-certs = "^0.6.3" 49 | rustls-pemfile = "^1.0.3" 50 | semver = "^0.10.0" 51 | serde = "^1.0.145" 52 | serde_derive = "^1.0.114" 53 | serde_json = { workspace = true } 54 | thiserror = "^1.0.37" 55 | tokio = { workspace = true } 56 | tokio-rustls = "^0.24.1" 57 | toml = "^0.5.9" 58 | tracing = { workspace = true } 59 | tracing-futures = { workspace = true } 60 | url = { workspace = true } 61 | wasmparser = { workspace = true } 62 | wasm-encoder = { workspace = true } 63 | wit-component = { workspace = true } 64 | wasmtime = { workspace = true } 65 | wasmtime-wasi = { workspace = true } 66 | wasmtime-wasi-nn = { workspace = true } 67 | wat = { workspace = true } 68 | wiggle = { workspace = true } 69 | base64 = { workspace = true } 70 | moka = { version = "0.12.10", features = ["future"] } 71 | 72 | [dev-dependencies] 73 | proptest = "1.6.0" 74 | proptest-derive = "0.5.1" 75 | tempfile = "3.6.0" 76 | 77 | [features] 78 | default = [] 79 | test-fatalerror-config = [] 80 | -------------------------------------------------------------------------------- /lib/compute-at-edge-abi/README.md: -------------------------------------------------------------------------------- 1 | # 🔗 compute-at-edge-abi 2 | 3 | This directory contains the canonical `witx` definitions for the Compute 4 | platform ABI. 5 | 6 | ### About `witx` 7 | 8 | > The `witx` file format is an experimental format which is based on the 9 | > [module linking] text format (`wit`), (which is in turn based on the 10 | > [wat format], which is based on [S-expressions]). It adds some features 11 | > using the same syntax as [interface types], some features with syntax 12 | > similar to [gc types], as well as a few special features of its own. 13 | > 14 | > `witx` is actively evolving. Expect backwards-incompatible changes, 15 | > particularly in the areas where `witx` differs from `wit`. 16 | > 17 | > The initial goal for `witx` is just to have a language suitable for 18 | > expressing [WASI] APIs in, to serve as the vocabulary for proposing changes 19 | > to existing APIs and proposing new APIs. Initially, while it uses some of 20 | > the syntax and concepts from interface types, it doesn't currently imply the 21 | > full interface types specification, or the use of the interface types custom 22 | > sections. 23 | > 24 | > We expect that eventually we will transition to using the full interface 25 | > types specification, with `witx` having minimal additional features. Until 26 | > then, the goals here are to remain aligned with interface types and other 27 | > relevant WebAssembly standards and proposals wherever practical, and to be an 28 | > input into the design process of interface types. 29 | 30 | - [source][witx] 31 | 32 | [interface types]: https://github.com/WebAssembly/interface-types/blob/master/proposals/interface-types/Explainer.md 33 | [gc types]: https://github.com/WebAssembly/gc 34 | [module linking]: https://github.com/WebAssembly/module-linking/blob/master/proposals/module-linking/Explainer.md 35 | [S-expressions]: https://en.wikipedia.org/wiki/S-expression 36 | [WASI]: https://github.com/WebAssembly/WASI 37 | [wat format]: https://webassembly.github.io/spec/core/bikeshed/index.html#text-format%E2%91%A0 38 | [witx]: https://github.com/WebAssembly/WASI/blob/main/docs/witx.md 39 | -------------------------------------------------------------------------------- /lib/compute-at-edge-abi/config-store.witx: -------------------------------------------------------------------------------- 1 | ;; Config Store ABI 2 | 3 | ;;; A handle to an Config Store. 4 | (typename $config_store_handle (handle)) 5 | 6 | (module $fastly_config_store 7 | (@interface func (export "open") 8 | (param $name string) 9 | (result $err (expected $config_store_handle (error $fastly_status))) 10 | ) 11 | 12 | (@interface func (export "get") 13 | (param $h $config_store_handle) 14 | (param $key string) 15 | (param $value (@witx pointer (@witx char8))) 16 | (param $value_max_len (@witx usize)) 17 | (param $nwritten_out (@witx pointer (@witx usize))) 18 | (result $err (expected (error $fastly_status))) 19 | ) 20 | ) 21 | -------------------------------------------------------------------------------- /lib/compute-at-edge-abi/shielding.witx: -------------------------------------------------------------------------------- 1 | (typename $shield_backend_options 2 | (flags (@witx repr u32) 3 | $reserved 4 | $use_cache_key 5 | )) 6 | 7 | (typename $shield_backend_config 8 | (record 9 | (field $cache_key (@witx pointer (@witx char8))) 10 | (field $cache_key_len u32) 11 | )) 12 | 13 | (module $fastly_shielding 14 | 15 | (@interface func (export "shield_info") 16 | (param $name string) 17 | (param $info_block (@witx pointer (@witx char8))) 18 | (param $info_block_max_len (@witx usize)) 19 | (result $err (expected $num_bytes (error $fastly_status))) 20 | ) 21 | 22 | (@interface func (export "backend_for_shield") 23 | (param $shield_name string) 24 | (param $backend_config_mask $shield_backend_options) 25 | (param $backend_configuration (@witx pointer $shield_backend_config)) 26 | (param $backend_name_out (@witx pointer (@witx char8))) 27 | (param $backend_name_max_len (@witx usize)) 28 | (result $err (expected $num_bytes (error $fastly_status))) 29 | ) 30 | 31 | ) 32 | -------------------------------------------------------------------------------- /lib/data/viceroy-component-adapter.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastly/Viceroy/2b7227e19220ed072c525be80e7c4406ece5aba9/lib/data/viceroy-component-adapter.wasm -------------------------------------------------------------------------------- /lib/proptest-regressions/cache.txt: -------------------------------------------------------------------------------- 1 | # Seeds for failure cases proptest has generated in the past. It is 2 | # automatically read and these particular cases re-run before any 3 | # novel cases are generated. 4 | # 5 | # It is recommended to check this file in to source control so that 6 | # everyone who runs the test benefits from these saved cases. 7 | cc 59cac97c28e31b87da4163d47031bc52ca7e07f8e90887911c569a29fbdc75c0 # shrinks to key = CacheKey([]), max_age = 9223372036854481161.46386714s, initial_age = None, value = [] 8 | -------------------------------------------------------------------------------- /lib/src/async_io.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | error::Error, 4 | session::Session, 5 | wiggle_abi::{fastly_async_io::FastlyAsyncIo, types::AsyncItemHandle}, 6 | }, 7 | futures::{FutureExt, TryFutureExt}, 8 | std::time::Duration, 9 | tokio::time::timeout, 10 | wiggle::{GuestMemory, GuestPtr}, 11 | }; 12 | 13 | #[wiggle::async_trait] 14 | impl FastlyAsyncIo for Session { 15 | async fn select( 16 | &mut self, 17 | memory: &mut GuestMemory<'_>, 18 | handles: GuestPtr<[AsyncItemHandle]>, 19 | timeout_ms: u32, 20 | ) -> Result { 21 | let handles = handles.cast::<[u32]>(); 22 | if handles.len() == 0 && timeout_ms == 0 { 23 | return Err(Error::InvalidArgument); 24 | } 25 | 26 | let select_fut = self 27 | .select_impl( 28 | memory 29 | // TODO: `GuestMemory::as_slice` only supports guest pointers to u8 slices in 30 | // wiggle 22.0.0, but `GuestMemory::to_vec` supports guest pointers to slices 31 | // of arbitrary types. As `GuestMemory::to_vec` will copy the contents of the 32 | // slice out of guest memory, we should switch this to `GuestMemory::as_slice` 33 | // once it is polymorphic in the element type of the slice. 34 | .to_vec(handles)? 35 | .into_iter() 36 | .map(|i| AsyncItemHandle::from(i).into()), 37 | ) 38 | .map_ok(|done_idx| done_idx as u32); 39 | 40 | if timeout_ms == 0 { 41 | select_fut.await 42 | } else { 43 | timeout(Duration::from_millis(timeout_ms as u64), select_fut) 44 | .await 45 | .unwrap_or(Ok(u32::MAX)) 46 | } 47 | } 48 | fn is_ready( 49 | &mut self, 50 | _memory: &mut GuestMemory<'_>, 51 | handle: AsyncItemHandle, 52 | ) -> Result { 53 | if self 54 | .async_item_mut(handle.into())? 55 | .await_ready() 56 | .now_or_never() 57 | .is_some() 58 | { 59 | Ok(1) 60 | } else { 61 | Ok(0) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/src/component/acl.rs: -------------------------------------------------------------------------------- 1 | use super::fastly::api::{acl, http_body, types}; 2 | use crate::linking::ComponentCtx; 3 | use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; 4 | 5 | #[async_trait::async_trait] 6 | impl acl::Host for ComponentCtx { 7 | async fn open(&mut self, acl_name: Vec) -> Result { 8 | let acl_name = String::from_utf8(acl_name)?; 9 | let handle = self 10 | .session 11 | .acl_handle_by_name(&acl_name) 12 | .ok_or(types::Error::OptionalNone)?; 13 | Ok(handle.into()) 14 | } 15 | 16 | async fn lookup( 17 | &mut self, 18 | acl_handle: acl::AclHandle, 19 | ip_octets: Vec, 20 | ip_len: u64, 21 | ) -> Result<(Option, acl::AclError), types::Error> { 22 | let acl = self 23 | .session 24 | .acl_by_handle(acl_handle.into()) 25 | .ok_or(types::Error::BadHandle)?; 26 | 27 | let ip: IpAddr = match ip_len { 28 | 4 => IpAddr::V4(Ipv4Addr::from( 29 | TryInto::<[u8; 4]>::try_into(ip_octets).unwrap(), 30 | )), 31 | 16 => IpAddr::V6(Ipv6Addr::from( 32 | TryInto::<[u8; 16]>::try_into(ip_octets).unwrap(), 33 | )), 34 | _ => return Err(types::Error::InvalidArgument), 35 | }; 36 | 37 | match acl.lookup(ip) { 38 | Some(entry) => { 39 | let body = 40 | serde_json::to_vec_pretty(&entry).map_err(|_| types::Error::GenericError)?; 41 | let body_handle = self.session.insert_body(body.into()); 42 | Ok((Some(body_handle.into()), acl::AclError::Ok)) 43 | } 44 | None => Ok((None, acl::AclError::NoContent)), 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/component/async_io.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::fastly::api::{async_io, types}, 3 | crate::{linking::ComponentCtx, wiggle_abi}, 4 | futures::FutureExt, 5 | std::time::Duration, 6 | }; 7 | 8 | #[async_trait::async_trait] 9 | impl async_io::Host for ComponentCtx { 10 | async fn select( 11 | &mut self, 12 | hs: Vec, 13 | timeout_ms: u32, 14 | ) -> Result, types::Error> { 15 | if hs.is_empty() && timeout_ms == 0 { 16 | return Err(types::Error::InvalidArgument.into()); 17 | } 18 | 19 | let select_fut = self.session.select_impl( 20 | hs.iter() 21 | .copied() 22 | .map(|i| wiggle_abi::types::AsyncItemHandle::from(i).into()), 23 | ); 24 | 25 | if timeout_ms == 0 { 26 | let h = select_fut.await?; 27 | return Ok(Some(h as u32)); 28 | } 29 | 30 | let res = tokio::time::timeout(Duration::from_millis(timeout_ms as u64), select_fut).await; 31 | 32 | match res { 33 | // got a handle 34 | Ok(Ok(h)) => Ok(Some(h as u32)), 35 | 36 | // timeout elapsed 37 | Err(_) => Ok(None), 38 | 39 | // some other error happened, but the future resolved 40 | Ok(Err(e)) => Err(e.into()), 41 | } 42 | } 43 | 44 | async fn is_ready(&mut self, handle: async_io::Handle) -> Result { 45 | let handle = wiggle_abi::types::AsyncItemHandle::from(handle); 46 | Ok(self 47 | .session 48 | .async_item_mut(handle.into())? 49 | .await_ready() 50 | .now_or_never() 51 | .is_some()) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/component/compute_runtime.rs: -------------------------------------------------------------------------------- 1 | use super::fastly::api::{compute_runtime, types}; 2 | use crate::linking::ComponentCtx; 3 | use std::sync::atomic::Ordering; 4 | 5 | #[async_trait::async_trait] 6 | impl compute_runtime::Host for ComponentCtx { 7 | async fn get_vcpu_ms(&mut self) -> Result { 8 | Ok(self.session.active_cpu_time_us.load(Ordering::SeqCst) / 1000) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/component/config_store.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::fastly::api::{config_store, types}, 3 | crate::linking::ComponentCtx, 4 | }; 5 | 6 | #[async_trait::async_trait] 7 | impl config_store::Host for ComponentCtx { 8 | async fn open(&mut self, name: String) -> Result { 9 | let handle = self.session.dictionary_handle(name.as_str())?; 10 | Ok(handle.into()) 11 | } 12 | 13 | async fn get( 14 | &mut self, 15 | store: config_store::Handle, 16 | name: String, 17 | max_len: u64, 18 | ) -> Result>, types::Error> { 19 | let dict = &self.session.dictionary(store.into())?.contents; 20 | 21 | let item = if let Some(item) = dict.get(&name) { 22 | item 23 | } else { 24 | return Ok(None); 25 | }; 26 | 27 | if item.len() > usize::try_from(max_len).unwrap() { 28 | return Err(types::Error::BufferLen(u64::try_from(item.len()).unwrap())); 29 | } 30 | 31 | Ok(Some(item.as_bytes().to_owned())) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/component/device_detection.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::fastly::api::{device_detection, types}, 3 | crate::linking::ComponentCtx, 4 | }; 5 | 6 | #[async_trait::async_trait] 7 | impl device_detection::Host for ComponentCtx { 8 | async fn lookup( 9 | &mut self, 10 | user_agent: String, 11 | max_len: u64, 12 | ) -> Result>, types::Error> { 13 | if let Some(result) = self.session.device_detection_lookup(&user_agent) { 14 | if result.len() > max_len as usize { 15 | return Err(types::Error::BufferLen( 16 | u64::try_from(result.len()).unwrap_or(0), 17 | )); 18 | } 19 | 20 | Ok(Some(result.into_bytes())) 21 | } else { 22 | Ok(None) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/component/dictionary.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::fastly::api::{dictionary, types}, 3 | crate::linking::ComponentCtx, 4 | }; 5 | 6 | #[async_trait::async_trait] 7 | impl dictionary::Host for ComponentCtx { 8 | async fn open(&mut self, name: String) -> Result { 9 | let handle = self.session.dictionary_handle(name.as_str())?; 10 | Ok(handle.into()) 11 | } 12 | 13 | async fn get( 14 | &mut self, 15 | h: dictionary::Handle, 16 | key: String, 17 | max_len: u64, 18 | ) -> Result>, types::Error> { 19 | let dict = &self.session.dictionary(h.into())?.contents; 20 | 21 | let item = if let Some(item) = dict.get(&key) { 22 | item 23 | } else { 24 | return Ok(None); 25 | }; 26 | 27 | if item.len() > usize::try_from(max_len).unwrap() { 28 | return Err(types::Error::BufferLen(u64::try_from(item.len()).unwrap())); 29 | } 30 | 31 | Ok(Some(item.as_bytes().to_owned())) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/component/erl.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::fastly::api::{erl, types}, 3 | crate::linking::ComponentCtx, 4 | }; 5 | 6 | #[async_trait::async_trait] 7 | impl erl::Host for ComponentCtx { 8 | async fn check_rate( 9 | &mut self, 10 | _rc: String, 11 | _entry: String, 12 | _delta: u32, 13 | _window: u32, 14 | _limit: u32, 15 | _pb: String, 16 | _ttl: u32, 17 | ) -> Result { 18 | Ok(0) 19 | } 20 | 21 | async fn ratecounter_increment( 22 | &mut self, 23 | _rc: String, 24 | _entry: String, 25 | _delta: u32, 26 | ) -> Result<(), types::Error> { 27 | Ok(()) 28 | } 29 | 30 | async fn ratecounter_lookup_rate( 31 | &mut self, 32 | _rc: String, 33 | _entry: String, 34 | _window: u32, 35 | ) -> Result { 36 | Ok(0) 37 | } 38 | 39 | async fn ratecounter_lookup_count( 40 | &mut self, 41 | _rc: String, 42 | _entry: String, 43 | _duration: u32, 44 | ) -> Result { 45 | Ok(0) 46 | } 47 | 48 | async fn penaltybox_add( 49 | &mut self, 50 | _pb: String, 51 | _entry: String, 52 | _ttl: u32, 53 | ) -> Result<(), types::Error> { 54 | Ok(()) 55 | } 56 | 57 | async fn penaltybox_has(&mut self, _pb: String, _entry: String) -> Result { 58 | Ok(0) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/component/geo.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::fastly::api::{geo, types}, 3 | crate::{error, linking::ComponentCtx}, 4 | std::net::{IpAddr, Ipv4Addr, Ipv6Addr}, 5 | }; 6 | 7 | #[async_trait::async_trait] 8 | impl geo::Host for ComponentCtx { 9 | async fn lookup(&mut self, octets: Vec, max_len: u64) -> Result, types::Error> { 10 | let ip_addr: IpAddr = match octets.len() { 11 | 4 => IpAddr::V4(Ipv4Addr::from( 12 | TryInto::<[u8; 4]>::try_into(octets).unwrap(), 13 | )), 14 | 16 => IpAddr::V6(Ipv6Addr::from( 15 | TryInto::<[u8; 16]>::try_into(octets).unwrap(), 16 | )), 17 | _ => return Err(error::Error::InvalidArgument.into()), 18 | }; 19 | 20 | let json = self 21 | .session 22 | .geolocation_lookup(&ip_addr) 23 | .ok_or(geo::Error::UnknownError)?; 24 | 25 | if json.len() > usize::try_from(max_len).unwrap() { 26 | return Err(error::Error::BufferLengthError { 27 | buf: "geo_out", 28 | len: "geo_max_len", 29 | } 30 | .into()); 31 | } 32 | 33 | Ok(json.into_bytes()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/component/headers.rs: -------------------------------------------------------------------------------- 1 | type MultiValueCursor = u32; 2 | 3 | /// Write multiple values out to a single buffer, until the iterator is exhausted, or `max_len` 4 | /// bytes have been written. In the case that there are still values remaining, the second value of 5 | /// the returned tuple will be `Some`. 6 | /// 7 | /// If it's not possible to fit a single value inside a buffer of length `max_len`, an error will 8 | /// be returned with the size necessary for the first element of the collection. 9 | pub fn write_values( 10 | iter: I, 11 | terminator: u8, 12 | max_len: usize, 13 | cursor_start: MultiValueCursor, 14 | ) -> Result<(Vec, Option), usize> 15 | where 16 | I: Iterator, 17 | T: AsRef<[u8]>, 18 | { 19 | let mut buf = Vec::with_capacity(max_len); 20 | 21 | let mut cursor = cursor_start; 22 | let mut finished = true; 23 | let skip_amt = usize::try_from(cursor).expect("u32 can fit in usize"); 24 | for item in iter.skip(skip_amt) { 25 | let bytes = item.as_ref(); 26 | 27 | let needed = buf.len() + bytes.len() + 1; 28 | if needed > max_len { 29 | // If we haven't written a single entry yet, return an error indicating how much space 30 | // we would need to write a single entry. 31 | if cursor == cursor_start { 32 | return Err(needed); 33 | } 34 | 35 | finished = false; 36 | break; 37 | } 38 | 39 | buf.extend(bytes); 40 | buf.push(terminator); 41 | 42 | cursor += 1 43 | } 44 | 45 | let cursor = if finished { None } else { Some(cursor) }; 46 | 47 | Ok((buf, cursor)) 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/component/http_types.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::fastly::api::{http_types, types}, 3 | crate::linking::ComponentCtx, 4 | }; 5 | 6 | impl http_types::Host for ComponentCtx {} 7 | 8 | // The http crate's `Version` is a struct that has a bunch of 9 | // associated constants, not an enum; this is only a partial conversion. 10 | impl TryFrom for http_types::HttpVersion { 11 | type Error = types::Error; 12 | fn try_from(v: http::version::Version) -> Result { 13 | match v { 14 | http::version::Version::HTTP_09 => Ok(http_types::HttpVersion::Http09), 15 | http::version::Version::HTTP_10 => Ok(http_types::HttpVersion::Http10), 16 | http::version::Version::HTTP_11 => Ok(http_types::HttpVersion::Http11), 17 | http::version::Version::HTTP_2 => Ok(http_types::HttpVersion::H2), 18 | http::version::Version::HTTP_3 => Ok(http_types::HttpVersion::H3), 19 | _ => Err(types::Error::Unsupported), 20 | } 21 | } 22 | } 23 | 24 | impl From for http::version::Version { 25 | fn from(v: http_types::HttpVersion) -> http::version::Version { 26 | match v { 27 | http_types::HttpVersion::Http09 => http::version::Version::HTTP_09, 28 | http_types::HttpVersion::Http10 => http::version::Version::HTTP_10, 29 | http_types::HttpVersion::Http11 => http::version::Version::HTTP_11, 30 | http_types::HttpVersion::H2 => http::version::Version::HTTP_2, 31 | http_types::HttpVersion::H3 => http::version::Version::HTTP_3, 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/component/image_optimizer.rs: -------------------------------------------------------------------------------- 1 | use super::fastly::api::{http_req, http_types, image_optimizer, types}; 2 | use crate::linking::ComponentCtx; 3 | 4 | #[async_trait::async_trait] 5 | impl image_optimizer::Host for ComponentCtx { 6 | async fn transform_image_optimizer_request( 7 | &mut self, 8 | _origin_image_request: http_req::RequestHandle, 9 | _origin_image_request_body: http_req::BodyHandle, 10 | _origin_image_request_backend: Vec, 11 | _io_transform_config_mask: image_optimizer::ImageOptimizerTransformConfigOptions, 12 | _io_transform_config: image_optimizer::ImageOptimizerTransformConfig, 13 | _io_error_detail: image_optimizer::ImageOptimizerErrorDetail, 14 | ) -> Result { 15 | Err(types::Error::Unsupported) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/component/log.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::fastly::api::{log, types}, 3 | crate::linking::ComponentCtx, 4 | lazy_static::lazy_static, 5 | }; 6 | 7 | fn is_reserved_endpoint(name: &[u8]) -> bool { 8 | use regex::bytes::{RegexSet, RegexSetBuilder}; 9 | const RESERVED_ENDPOINTS: &[&str] = &["^stdout$", "^stderr$", "^fst_managed_"]; 10 | lazy_static! { 11 | static ref RESERVED_ENDPOINT_RE: RegexSet = RegexSetBuilder::new(RESERVED_ENDPOINTS) 12 | .case_insensitive(true) 13 | .build() 14 | .unwrap(); 15 | } 16 | RESERVED_ENDPOINT_RE.is_match(name) 17 | } 18 | 19 | #[async_trait::async_trait] 20 | impl log::Host for ComponentCtx { 21 | async fn endpoint_get(&mut self, name: String) -> Result { 22 | let name = name.as_bytes(); 23 | 24 | if is_reserved_endpoint(name) { 25 | return Err(types::Error::InvalidArgument.into()); 26 | } 27 | 28 | Ok(self.session.log_endpoint_handle(name).into()) 29 | } 30 | 31 | async fn write(&mut self, h: log::Handle, msg: String) -> Result { 32 | let endpoint = self.session.log_endpoint(h.into())?; 33 | let msg = msg.as_bytes(); 34 | endpoint.write_entry(&msg)?; 35 | Ok(u32::try_from(msg.len()).unwrap()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/component/purge.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::fastly::api::{purge, types}, 3 | crate::{error::Error, linking::ComponentCtx}, 4 | }; 5 | 6 | #[async_trait::async_trait] 7 | impl purge::Host for ComponentCtx { 8 | async fn purge_surrogate_key( 9 | &mut self, 10 | _surrogate_key: String, 11 | _options: purge::PurgeOptionsMask, 12 | _max_len: u64, 13 | ) -> Result, types::Error> { 14 | Err(Error::NotAvailable("FastlyPurge").into()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/component/secret_store.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::fastly::api::{secret_store, types}, 3 | crate::{ 4 | error::Error, linking::ComponentCtx, secret_store::SecretLookup, 5 | wiggle_abi::SecretStoreError, 6 | }, 7 | }; 8 | 9 | #[async_trait::async_trait] 10 | impl secret_store::Host for ComponentCtx { 11 | async fn open(&mut self, name: String) -> Result { 12 | let handle = self 13 | .session 14 | .secret_store_handle(&name) 15 | .ok_or(Error::SecretStoreError( 16 | SecretStoreError::UnknownSecretStore(name.to_string()), 17 | ))?; 18 | Ok(handle.into()) 19 | } 20 | 21 | async fn get( 22 | &mut self, 23 | store: secret_store::StoreHandle, 24 | key: String, 25 | ) -> Result, types::Error> { 26 | let store_name = self 27 | .session 28 | .secret_store_name(store.into()) 29 | .ok_or_else(|| { 30 | types::Error::from(SecretStoreError::InvalidSecretStoreHandle(store.into())) 31 | })?; 32 | Ok(self 33 | .session 34 | .secret_handle(&store_name, &key) 35 | .map(secret_store::SecretHandle::from)) 36 | } 37 | 38 | async fn plaintext( 39 | &mut self, 40 | secret: secret_store::SecretHandle, 41 | max_len: u64, 42 | ) -> Result>, types::Error> { 43 | let lookup = self 44 | .session 45 | .secret_lookup(secret.into()) 46 | .ok_or(Error::SecretStoreError( 47 | SecretStoreError::InvalidSecretHandle(secret.into()), 48 | ))?; 49 | 50 | let plaintext = match &lookup { 51 | SecretLookup::Standard { 52 | store_name, 53 | secret_name, 54 | } => self 55 | .session 56 | .secret_stores() 57 | .get_store(store_name) 58 | .ok_or(Error::SecretStoreError( 59 | SecretStoreError::InvalidSecretHandle(secret.into()), 60 | ))? 61 | .get_secret(secret_name) 62 | .ok_or(Error::SecretStoreError( 63 | SecretStoreError::InvalidSecretHandle(secret.into()), 64 | ))? 65 | .plaintext(), 66 | 67 | SecretLookup::Injected { plaintext } => plaintext, 68 | }; 69 | 70 | if plaintext.len() > usize::try_from(max_len).unwrap() { 71 | return Err(Error::BufferLengthError { 72 | buf: "plaintext", 73 | len: "plaintext_max_len", 74 | } 75 | .into()); 76 | } 77 | 78 | Ok(Some(plaintext.to_owned())) 79 | } 80 | 81 | async fn from_bytes( 82 | &mut self, 83 | plaintext: Vec, 84 | ) -> Result { 85 | Ok(self.session.add_secret(plaintext).into()) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/src/component/types.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::fastly::api::types, 3 | crate::{ 4 | error::{self, HandleError}, 5 | linking::ComponentCtx, 6 | }, 7 | http::header::InvalidHeaderName, 8 | }; 9 | 10 | pub enum TrappableError { 11 | Error(types::Error), 12 | Trap(anyhow::Error), 13 | } 14 | 15 | impl types::Host for ComponentCtx { 16 | fn convert_error(&mut self, err: TrappableError) -> wasmtime::Result { 17 | match err { 18 | TrappableError::Error(err) => Ok(err), 19 | TrappableError::Trap(err) => Err(err), 20 | } 21 | } 22 | } 23 | 24 | impl From for TrappableError { 25 | fn from(e: wasmtime::component::ResourceTableError) -> Self { 26 | Self::Trap(e.into()) 27 | } 28 | } 29 | 30 | impl From for TrappableError { 31 | fn from(e: types::Error) -> Self { 32 | Self::Error(e) 33 | } 34 | } 35 | 36 | impl From for TrappableError { 37 | fn from(_: HandleError) -> Self { 38 | Self::Error(types::Error::BadHandle) 39 | } 40 | } 41 | 42 | impl From for TrappableError { 43 | fn from(_: InvalidHeaderName) -> Self { 44 | Self::Error(types::Error::GenericError) 45 | } 46 | } 47 | 48 | impl From for TrappableError { 49 | fn from(e: error::Error) -> Self { 50 | match e { 51 | error::Error::FatalError(_) => Self::Trap(anyhow::anyhow!(e.to_string())), 52 | _ => Self::Error(e.into()), 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/component/uap.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::fastly::api::{types, uap}, 3 | crate::{error::Error, linking::ComponentCtx}, 4 | wasmtime::component::Resource, 5 | }; 6 | 7 | #[derive(Debug)] 8 | pub struct UserAgent {} 9 | 10 | #[async_trait::async_trait] 11 | impl uap::HostUserAgent for ComponentCtx { 12 | async fn family( 13 | &mut self, 14 | _agent: Resource, 15 | _max_len: u64, 16 | ) -> Result { 17 | Err(Error::NotAvailable("User-agent parsing is not available").into()) 18 | } 19 | 20 | async fn major( 21 | &mut self, 22 | _agent: Resource, 23 | _max_len: u64, 24 | ) -> Result { 25 | Err(Error::NotAvailable("User-agent parsing is not available").into()) 26 | } 27 | 28 | async fn minor( 29 | &mut self, 30 | _agent: Resource, 31 | _max_len: u64, 32 | ) -> Result { 33 | Err(Error::NotAvailable("User-agent parsing is not available").into()) 34 | } 35 | 36 | async fn patch( 37 | &mut self, 38 | _agent: Resource, 39 | _max_len: u64, 40 | ) -> Result { 41 | Err(Error::NotAvailable("User-agent parsing is not available").into()) 42 | } 43 | 44 | async fn drop(&mut self, _agent: Resource) -> wasmtime::Result<()> { 45 | Err(Error::NotAvailable("User-agent parsing is not available").into()) 46 | } 47 | } 48 | 49 | #[async_trait::async_trait] 50 | impl uap::Host for ComponentCtx { 51 | async fn parse(&mut self, _user_agent: String) -> Result, types::Error> { 52 | // not available 53 | Err(Error::NotAvailable("User-agent parsing is not available").into()) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/config/acl.rs: -------------------------------------------------------------------------------- 1 | use crate::acl; 2 | 3 | #[derive(Clone, Debug, Default)] 4 | pub struct AclConfig(pub(crate) acl::Acls); 5 | 6 | mod deserialization { 7 | use { 8 | super::AclConfig, 9 | crate::acl, 10 | crate::error::{AclConfigError, FastlyConfigError}, 11 | std::path::PathBuf, 12 | std::{convert::TryFrom, fs}, 13 | toml::value::Table, 14 | }; 15 | 16 | impl TryFrom for AclConfig { 17 | type Error = FastlyConfigError; 18 | fn try_from(toml: Table) -> Result { 19 | let mut acls = acl::Acls::new(); 20 | 21 | for (name, value) in toml.iter() { 22 | // Here we allow each table entry to be either a: 23 | // - string: path to JSON file 24 | // - table: must have a 'file' entry, which is the path to JSON file 25 | let path = if let Some(path) = value.as_str() { 26 | path 27 | } else if let Some(tbl) = value.as_table() { 28 | tbl.get("file") 29 | .ok_or(FastlyConfigError::InvalidAclDefinition { 30 | name: name.to_string(), 31 | err: AclConfigError::MissingFile, 32 | })? 33 | .as_str() 34 | .ok_or(FastlyConfigError::InvalidAclDefinition { 35 | name: name.to_string(), 36 | err: AclConfigError::MissingFile, 37 | })? 38 | } else { 39 | return Err(FastlyConfigError::InvalidAclDefinition { 40 | name: name.to_string(), 41 | err: AclConfigError::InvalidType, 42 | }); 43 | }; 44 | 45 | let acl: acl::Acl = { 46 | let path = PathBuf::from(path); 47 | let fd = fs::File::open(path).map_err(|err| { 48 | FastlyConfigError::InvalidAclDefinition { 49 | name: name.to_string(), 50 | err: AclConfigError::IoError(err), 51 | } 52 | })?; 53 | serde_json::from_reader(fd).map_err(|err| { 54 | FastlyConfigError::InvalidAclDefinition { 55 | name: name.to_string(), 56 | err: AclConfigError::JsonError(err), 57 | } 58 | })? 59 | }; 60 | 61 | acls.insert(name.to_string(), acl); 62 | } 63 | 64 | Ok(Self(acls)) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/src/config/limits.rs: -------------------------------------------------------------------------------- 1 | // From https://docs.fastly.com/en/guides/resource-limits#vcl-and-configuration-limits 2 | pub const DICTIONARY_ITEM_KEY_MAX_LEN: usize = 256; 3 | pub const DICTIONARY_ITEM_VALUE_MAX_LEN: usize = 8000; 4 | -------------------------------------------------------------------------------- /lib/src/downstream.rs: -------------------------------------------------------------------------------- 1 | //! Operations related to handling the "downstream" (end-client) request 2 | use std::net::SocketAddr; 3 | 4 | use crate::body::Body; 5 | use crate::error::DownstreamRequestError; 6 | use http::Request; 7 | use hyper::Uri; 8 | use tokio::sync::oneshot::Sender; 9 | 10 | pub struct DownstreamMetadata { 11 | // A unique request ID. 12 | pub req_id: u64, 13 | /// The IP address and port that received this request. 14 | pub server_addr: SocketAddr, 15 | /// The downstream IP address and port for this request. 16 | pub client_addr: SocketAddr, 17 | /// The compliance region that this request was received in. 18 | /// 19 | /// For now this is just always `"none"`, but we place the field in the session 20 | /// to make it easier to implement custom configuration values later on. 21 | pub compliance_region: Vec, 22 | } 23 | 24 | pub struct DownstreamRequest { 25 | pub req: hyper::Request, 26 | pub metadata: DownstreamMetadata, 27 | pub sender: Sender>, 28 | } 29 | 30 | /// Canonicalize the incoming request into the form expected by host calls. 31 | /// 32 | /// The primary canonicalization is to provide an absolute URL (with authority), using the HOST 33 | /// header of the request. 34 | pub fn prepare_request(req: Request) -> Result, DownstreamRequestError> { 35 | let (mut metadata, body) = req.into_parts(); 36 | let uri_parts = metadata.uri.into_parts(); 37 | 38 | // Prefer to find the host from the HOST header, rather than the URL. 39 | let http_host = if let Some(host_header) = metadata.headers.get(http::header::HOST) { 40 | std::str::from_utf8(host_header.as_bytes()) 41 | .map_err(|_| DownstreamRequestError::InvalidHost)? 42 | } else { 43 | uri_parts 44 | .authority 45 | .as_ref() 46 | .ok_or(DownstreamRequestError::InvalidHost)? 47 | .host() 48 | }; 49 | 50 | // rebuild the request URI, replacing the authority with only the host and ensuring there is 51 | // a path and scheme 52 | let path_and_query = uri_parts 53 | .path_and_query 54 | .ok_or(DownstreamRequestError::InvalidUrl)?; 55 | let scheme = uri_parts.scheme.unwrap_or(http::uri::Scheme::HTTP); 56 | metadata.uri = Uri::builder() 57 | .scheme(scheme) 58 | .authority(http_host) 59 | .path_and_query(path_and_query) 60 | .build() 61 | .map_err(|_| DownstreamRequestError::InvalidUrl)?; 62 | 63 | Ok(Request::from_parts(metadata, Body::from(body))) 64 | } 65 | -------------------------------------------------------------------------------- /lib/src/headers.rs: -------------------------------------------------------------------------------- 1 | use hyper::{header, HeaderMap}; 2 | 3 | pub fn filter_outgoing_headers(headers: &mut HeaderMap) { 4 | // Remove framing-related headers; we rely on Hyper to insert the appropriate 5 | // framing headers automatically, and do not allow guests to include them. 6 | headers.remove(header::CONTENT_LENGTH); 7 | headers.remove(header::TRANSFER_ENCODING); 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Viceroy implementation details. 2 | 3 | // When building the project in release mode: 4 | // (1): Promote warnings into errors. 5 | // (2): Deny broken documentation links. 6 | // (3): Deny invalid codeblock attributes in documentation. 7 | // (4): Promote warnings in examples into errors, except for unused variables. 8 | #![cfg_attr(not(debug_assertions), deny(warnings))] 9 | #![cfg_attr(not(debug_assertions), deny(clippy::all))] 10 | #![cfg_attr(not(debug_assertions), deny(broken_intra_doc_links))] 11 | #![cfg_attr(not(debug_assertions), deny(invalid_codeblock_attributes))] 12 | #![cfg_attr(not(debug_assertions), doc(test(attr(deny(warnings)))))] 13 | #![cfg_attr(not(debug_assertions), doc(test(attr(allow(dead_code)))))] 14 | #![cfg_attr(not(debug_assertions), doc(test(attr(allow(unused_variables)))))] 15 | 16 | pub mod adapt; 17 | pub mod body; 18 | pub mod cache; 19 | pub mod config; 20 | pub mod error; 21 | pub mod logging; 22 | pub mod session; 23 | 24 | mod acl; 25 | mod async_io; 26 | mod collecting_body; 27 | pub mod component; 28 | mod downstream; 29 | mod execute; 30 | mod headers; 31 | mod linking; 32 | mod object_store; 33 | mod secret_store; 34 | mod service; 35 | mod shielding_site; 36 | mod streaming_body; 37 | mod upstream; 38 | pub mod wiggle_abi; 39 | 40 | pub use { 41 | error::Error, execute::ExecuteCtx, execute::GuestProfileConfig, service::ViceroyService, 42 | upstream::BackendConnector, wasmtime::ProfilingStrategy, 43 | }; 44 | -------------------------------------------------------------------------------- /lib/src/secret_store.rs: -------------------------------------------------------------------------------- 1 | use {bytes::Bytes, std::collections::HashMap}; 2 | 3 | #[derive(Clone, Debug, Default)] 4 | pub struct SecretStores { 5 | stores: HashMap, 6 | } 7 | 8 | impl SecretStores { 9 | pub fn new() -> Self { 10 | Self { 11 | stores: HashMap::new(), 12 | } 13 | } 14 | 15 | pub fn get_store(&self, name: &str) -> Option<&SecretStore> { 16 | self.stores.get(name) 17 | } 18 | 19 | pub fn add_store(&mut self, name: String, store: SecretStore) { 20 | self.stores.insert(name, store); 21 | } 22 | } 23 | 24 | #[derive(Clone, Debug, Default)] 25 | pub struct SecretStore { 26 | secrets: HashMap, 27 | } 28 | 29 | impl SecretStore { 30 | pub fn new() -> Self { 31 | Self { 32 | secrets: HashMap::new(), 33 | } 34 | } 35 | 36 | pub fn get_secret(&self, name: &str) -> Option<&Secret> { 37 | self.secrets.get(name) 38 | } 39 | 40 | pub fn add_secret(&mut self, name: String, secret: Bytes) { 41 | self.secrets.insert(name, Secret { plaintext: secret }); 42 | } 43 | } 44 | 45 | #[derive(Clone, Debug, Default)] 46 | pub struct Secret { 47 | plaintext: Bytes, 48 | } 49 | 50 | impl Secret { 51 | pub fn plaintext(&self) -> &[u8] { 52 | &self.plaintext 53 | } 54 | } 55 | 56 | #[derive(Clone, Debug)] 57 | pub enum SecretLookup { 58 | Standard { 59 | store_name: String, 60 | secret_name: String, 61 | }, 62 | Injected { 63 | plaintext: Vec, 64 | }, 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/session/downstream.rs: -------------------------------------------------------------------------------- 1 | //! Downstream response. 2 | 3 | use { 4 | crate::{body::Body, error::Error, headers::filter_outgoing_headers}, 5 | hyper::http::response::Response, 6 | std::mem, 7 | tokio::sync::oneshot::Sender, 8 | }; 9 | 10 | /// Downstream response states. 11 | /// 12 | /// See [`Session::set_downstream_response_sender`][set] and 13 | /// [`Session::send_downstream_response`][send] for more information. 14 | /// 15 | /// [send]: struct.Session.html#method.send_downstream_response 16 | /// [set]: struct.Session.html#method.set_downstream_response_sender 17 | pub(super) enum DownstreamResponse { 18 | /// No channel to send the response has been opened yet. 19 | Closed, 20 | /// A channel has been opened, but no response has been sent yet. 21 | Pending(Sender>), 22 | /// A response has already been sent downstream. 23 | Sent, 24 | } 25 | 26 | impl DownstreamResponse { 27 | /// Open a channel to send a [`Response`][resp] downstream, given a [`oneshot::Sender`][sender]. 28 | /// 29 | /// [resp]: https://docs.rs/http/latest/http/response/struct.Response.html 30 | /// [sender]: https://docs.rs/tokio/latest/tokio/sync/oneshot/struct.Sender.html 31 | pub fn new(sender: Sender>) -> Self { 32 | DownstreamResponse::Pending(sender) 33 | } 34 | 35 | /// Send a [`Response`][resp] downstream. 36 | /// 37 | /// Yield an error if a response has already been sent. 38 | /// 39 | /// # Panics 40 | /// 41 | /// This method will panic if the associated receiver was dropped prematurely. 42 | /// 43 | /// [resp]: https://docs.rs/http/latest/http/response/struct.Response.html 44 | pub fn send(&mut self, mut response: Response) -> Result<(), Error> { 45 | use DownstreamResponse::{Closed, Pending, Sent}; 46 | 47 | filter_outgoing_headers(response.headers_mut()); 48 | 49 | // Mark this `DownstreamResponse` as having been sent, and match on the previous value. 50 | match mem::replace(self, Sent) { 51 | Closed => panic!("downstream response channel was closed"), 52 | Pending(sender) => sender 53 | .send(response) 54 | .map_err(|_| ()) 55 | .expect("response receiver is open"), 56 | Sent => return Err(Error::DownstreamRespSending), 57 | } 58 | 59 | Ok(()) 60 | } 61 | 62 | /// Close the `DownstreamResponse`, potentially without sending any response. 63 | pub fn close(&mut self) { 64 | *self = DownstreamResponse::Closed; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/src/wiggle_abi/acl.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Error, HandleError}; 2 | use crate::session::Session; 3 | use crate::wiggle_abi::{fastly_acl, types}; 4 | use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; 5 | 6 | #[wiggle::async_trait] 7 | impl fastly_acl::FastlyAcl for Session { 8 | /// Open a handle to an ACL by its linked name. 9 | fn open( 10 | &mut self, 11 | memory: &mut wiggle::GuestMemory<'_>, 12 | acl_name: wiggle::GuestPtr, 13 | ) -> Result { 14 | let acl_name = memory.as_str(acl_name)?.ok_or(Error::SharedMemory)?; 15 | self.acl_handle_by_name(acl_name).ok_or(Error::ValueAbsent) 16 | } 17 | 18 | /// Perform an ACL lookup operation using the given ACL handle. 19 | /// 20 | /// There are two levels of errors returned by this function: 21 | /// - Error: These are general hostcall errors, e.g. handle not found. 22 | /// - AclError: There are ACL-specific errors, e.g. 'no content'. 23 | /// It's the callers responsibility to check both errors. 24 | async fn lookup( 25 | &mut self, 26 | memory: &mut wiggle::GuestMemory<'_>, 27 | acl_handle: types::AclHandle, 28 | ip_octets: wiggle::GuestPtr, // This should be either a 4 or 16-byte array. 29 | ip_len: u32, // Either 4 or 16. 30 | body_handle_out: wiggle::GuestPtr, 31 | acl_error_out: wiggle::GuestPtr, 32 | ) -> Result<(), Error> { 33 | let acl = self.acl_by_handle(acl_handle).ok_or(Error::HandleError( 34 | HandleError::InvalidAclHandle(acl_handle), 35 | ))?; 36 | 37 | let ip: IpAddr = { 38 | let ip_octets = memory.to_vec(ip_octets.as_array(ip_len))?; 39 | match ip_len { 40 | 4 => IpAddr::V4(Ipv4Addr::from( 41 | TryInto::<[u8; 4]>::try_into(ip_octets).unwrap(), 42 | )), 43 | 16 => IpAddr::V6(Ipv6Addr::from( 44 | TryInto::<[u8; 16]>::try_into(ip_octets).unwrap(), 45 | )), 46 | _ => return Err(Error::InvalidArgument), 47 | } 48 | }; 49 | 50 | match acl.lookup(ip) { 51 | Some(entry) => { 52 | let body = 53 | serde_json::to_vec_pretty(&entry).map_err(|err| Error::Other(err.into()))?; 54 | let body_handle = self.insert_body(body.into()); 55 | memory.write(body_handle_out, body_handle)?; 56 | memory.write(acl_error_out, types::AclError::Ok)?; 57 | Ok(()) 58 | } 59 | None => { 60 | memory.write(acl_error_out, types::AclError::NoContent)?; 61 | Ok(()) 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/wiggle_abi/compute_runtime.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use crate::session::Session; 3 | use crate::wiggle_abi::fastly_compute_runtime::FastlyComputeRuntime; 4 | use std::sync::atomic::Ordering; 5 | use wiggle::GuestMemory; 6 | 7 | impl FastlyComputeRuntime for Session { 8 | fn get_vcpu_ms(&mut self, _memory: &mut GuestMemory<'_>) -> Result { 9 | // we internally track microseconds, because our wasmtime tick length 10 | // is too short for ms to work. but we want to shrink this to ms to 11 | // try to minimize timing attacks. 12 | Ok(self.active_cpu_time_us.load(Ordering::SeqCst) / 1000) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/wiggle_abi/config_store.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | fastly_config_store::FastlyConfigStore, 3 | fastly_dictionary::FastlyDictionary, 4 | types::{ConfigStoreHandle, DictionaryHandle}, 5 | }; 6 | use crate::{session::Session, Error}; 7 | use wiggle::{GuestMemory, GuestPtr}; 8 | 9 | impl FastlyConfigStore for Session { 10 | fn open( 11 | &mut self, 12 | memory: &mut GuestMemory<'_>, 13 | name: GuestPtr, 14 | ) -> Result { 15 | let dict_answer = ::open(self, memory, name)?; 16 | Ok(ConfigStoreHandle::from(unsafe { dict_answer.inner() })) 17 | } 18 | 19 | fn get( 20 | &mut self, 21 | memory: &mut GuestMemory<'_>, 22 | config_store: ConfigStoreHandle, 23 | key: GuestPtr, 24 | buf: GuestPtr, 25 | buf_len: u32, 26 | nwritten_out: GuestPtr, 27 | ) -> Result<(), Error> { 28 | let dict_handle = DictionaryHandle::from(unsafe { config_store.inner() }); 29 | ::get(self, memory, dict_handle, key, buf, buf_len, nwritten_out) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/wiggle_abi/device_detection_impl.rs: -------------------------------------------------------------------------------- 1 | //! fastly_device_detection` hostcall implementations. 2 | 3 | use crate::error::Error; 4 | use crate::wiggle_abi::{fastly_device_detection::FastlyDeviceDetection, FastlyStatus, Session}; 5 | use std::convert::TryFrom; 6 | use wiggle::{GuestMemory, GuestPtr}; 7 | 8 | #[derive(Debug, thiserror::Error)] 9 | pub enum DeviceDetectionError { 10 | /// Device detection data for given user_agent not found. 11 | #[error("No device detection data: {0}")] 12 | NoDeviceDetectionData(String), 13 | } 14 | 15 | impl DeviceDetectionError { 16 | /// Convert to an error code representation suitable for passing across the ABI boundary. 17 | pub fn to_fastly_status(&self) -> FastlyStatus { 18 | use DeviceDetectionError::*; 19 | match self { 20 | NoDeviceDetectionData(_) => FastlyStatus::None, 21 | } 22 | } 23 | } 24 | 25 | impl FastlyDeviceDetection for Session { 26 | fn lookup( 27 | &mut self, 28 | memory: &mut GuestMemory<'_>, 29 | user_agent: GuestPtr, 30 | buf: GuestPtr, 31 | buf_len: u32, 32 | nwritten_out: GuestPtr, 33 | ) -> Result<(), Error> { 34 | let result = { 35 | let user_agent_slice = memory 36 | .as_slice(user_agent.as_bytes())? 37 | .ok_or(Error::SharedMemory)?; 38 | let user_agent_str = std::str::from_utf8(&user_agent_slice)?; 39 | 40 | self.device_detection_lookup(user_agent_str) 41 | .ok_or_else(|| { 42 | DeviceDetectionError::NoDeviceDetectionData(user_agent_str.to_string()) 43 | })? 44 | }; 45 | 46 | if result.len() > buf_len as usize { 47 | memory.write(nwritten_out, u32::try_from(result.len()).unwrap_or(0))?; 48 | return Err(Error::BufferLengthError { 49 | buf: "device_detection_lookup", 50 | len: "device_detection_lookup_max_len", 51 | }); 52 | } 53 | 54 | let result_len = 55 | u32::try_from(result.len()).expect("smaller than buf_len means it must fit"); 56 | 57 | memory.copy_from_slice(result.as_bytes(), buf.as_array(result_len))?; 58 | 59 | memory.write(nwritten_out, result_len)?; 60 | Ok(()) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/wiggle_abi/dictionary_impl.rs: -------------------------------------------------------------------------------- 1 | //! fastly_dictionary` hostcall implementations. 2 | 3 | use { 4 | crate::{ 5 | error::Error, 6 | session::Session, 7 | wiggle_abi::{ 8 | fastly_dictionary::FastlyDictionary, 9 | types::{DictionaryHandle, FastlyStatus}, 10 | }, 11 | }, 12 | wiggle::{GuestMemory, GuestPtr}, 13 | }; 14 | 15 | #[derive(Debug, thiserror::Error)] 16 | pub enum DictionaryError { 17 | /// A dictionary item with the given key was not found. 18 | #[error("Unknown dictionary item: {0}")] 19 | UnknownDictionaryItem(String), 20 | /// A dictionary with the given name was not found. 21 | #[error("Unknown dictionary: {0}")] 22 | UnknownDictionary(String), 23 | } 24 | 25 | impl DictionaryError { 26 | /// Convert to an error code representation suitable for passing across the ABI boundary. 27 | pub fn to_fastly_status(&self) -> FastlyStatus { 28 | use DictionaryError::*; 29 | match self { 30 | UnknownDictionaryItem(_) => FastlyStatus::None, 31 | UnknownDictionary(_) => FastlyStatus::Badf, 32 | } 33 | } 34 | } 35 | 36 | impl FastlyDictionary for Session { 37 | fn open( 38 | &mut self, 39 | memory: &mut GuestMemory<'_>, 40 | name: GuestPtr, 41 | ) -> Result { 42 | self.dictionary_handle(memory.as_str(name)?.ok_or(Error::SharedMemory)?) 43 | } 44 | 45 | fn get( 46 | &mut self, 47 | memory: &mut GuestMemory<'_>, 48 | dictionary: DictionaryHandle, 49 | key: GuestPtr, 50 | buf: GuestPtr, 51 | buf_len: u32, 52 | nwritten_out: GuestPtr, 53 | ) -> Result<(), Error> { 54 | let dict = &self.dictionary(dictionary)?.contents; 55 | 56 | let item_bytes = { 57 | let key = memory.as_str(key)?.ok_or(Error::SharedMemory)?; 58 | dict.get(key) 59 | .ok_or_else(|| DictionaryError::UnknownDictionaryItem(key.to_owned()))? 60 | .as_bytes() 61 | }; 62 | 63 | if item_bytes.len() > usize::try_from(buf_len).expect("buf_len must fit in usize") { 64 | // Write out the number of bytes necessary to fit this item, or zero on overflow to 65 | // signal an error condition. This is probably unnecessary, as config store entries 66 | // may be at most 8000 utf-8 characters large. 67 | memory.write(nwritten_out, u32::try_from(item_bytes.len()).unwrap_or(0))?; 68 | return Err(Error::BufferLengthError { 69 | buf: "dictionary_item", 70 | len: "dictionary_item_max_len", 71 | }); 72 | } 73 | 74 | // We know the conversion of item_bytes.len() to u32 will succeed, as it's <= buf_len. 75 | let item_len = u32::try_from(item_bytes.len()).unwrap(); 76 | 77 | memory.write(nwritten_out, item_len)?; 78 | memory.copy_from_slice(item_bytes, buf.as_array(item_len))?; 79 | 80 | Ok(()) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/src/wiggle_abi/entity.rs: -------------------------------------------------------------------------------- 1 | //! [`cranelift_entity::EntityRef`][ref] implementations for ABI types. 2 | //! 3 | //! [ref]: https://docs.rs/cranelift-entity/latest/cranelift_entity/trait.EntityRef.html 4 | 5 | use super::types::{ 6 | AclHandle, AsyncItemHandle, BodyHandle, DictionaryHandle, EndpointHandle, KvStoreHandle, 7 | ObjectStoreHandle, PendingRequestHandle, RequestHandle, ResponseHandle, SecretHandle, 8 | SecretStoreHandle, 9 | }; 10 | 11 | /// Macro which implements a 32-bit entity reference for handles generated by Wiggle. 12 | /// 13 | /// This is for all intents and purposes, a use-case specific version of the [`entity_impl`][impl] 14 | /// macro provided by [`cranelift-entity`][entity]. For handles generated by a call to 15 | /// [`wiggle::from_witx`][from-witx], we have to implement entity reference trait slightly 16 | /// differently than normal, due to these types having a private constructor. Instead, we use their 17 | /// `From` trait implementations to convert back and forth from `usize` values. 18 | /// 19 | /// [entity]: https://docs.rs/cranelift-entity/latest/cranelift_entity/ 20 | /// [from-witx]: https://docs.rs/wiggle/latest/wiggle/macro.from_witx.html 21 | /// [impl]: https://docs.rs/cranelift-entity/latest/cranelift_entity/macro.entity_impl.html 22 | // TODO KTM 2020-06-29: If this ever becomes a maintenance burden, this could be submitted upstream 23 | // as an alternative mode for the `cranelift_entity::entity_impl` macro. 24 | macro_rules! wiggle_entity { 25 | ($entity:ident) => { 26 | /// `EntityRef` allows a small integer type to be used as the key to an entity map, such as 27 | /// `PrimaryMap`, `SecondaryMap`, or `SparseMap`. 28 | impl cranelift_entity::EntityRef for $entity { 29 | /// Create a new entity reference from a small integer. 30 | fn new(index: usize) -> Self { 31 | debug_assert!(index < (std::u32::MAX as usize)); 32 | (index as u32).into() 33 | } 34 | /// Get the index that was used to create this entity reference. 35 | fn index(self) -> usize { 36 | let i: u32 = self.into(); 37 | i as usize 38 | } 39 | } 40 | }; 41 | } 42 | 43 | wiggle_entity!(AclHandle); 44 | wiggle_entity!(AsyncItemHandle); 45 | wiggle_entity!(BodyHandle); 46 | wiggle_entity!(DictionaryHandle); 47 | wiggle_entity!(EndpointHandle); 48 | wiggle_entity!(KvStoreHandle); 49 | wiggle_entity!(ObjectStoreHandle); 50 | wiggle_entity!(PendingRequestHandle); 51 | wiggle_entity!(RequestHandle); 52 | wiggle_entity!(ResponseHandle); 53 | wiggle_entity!(SecretHandle); 54 | wiggle_entity!(SecretStoreHandle); 55 | -------------------------------------------------------------------------------- /lib/src/wiggle_abi/erl_impl.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::Error, 3 | session::Session, 4 | wiggle_abi::fastly_erl::FastlyErl, 5 | wiggle_abi::{GuestMemory, GuestPtr}, 6 | }; 7 | 8 | impl FastlyErl for Session { 9 | fn check_rate( 10 | &mut self, 11 | _memory: &mut GuestMemory<'_>, 12 | _rc: GuestPtr, 13 | _entry: GuestPtr, 14 | _delta: u32, 15 | _window: u32, 16 | _limit: u32, 17 | _pb: GuestPtr, 18 | _ttl: u32, 19 | ) -> std::result::Result { 20 | Ok(0) 21 | } 22 | 23 | fn ratecounter_increment( 24 | &mut self, 25 | _memory: &mut GuestMemory<'_>, 26 | _rc: GuestPtr, 27 | _entry: GuestPtr, 28 | _delta: u32, 29 | ) -> std::result::Result<(), Error> { 30 | Ok(()) 31 | } 32 | 33 | fn ratecounter_lookup_rate( 34 | &mut self, 35 | _memory: &mut GuestMemory<'_>, 36 | _rc: GuestPtr, 37 | _entry: GuestPtr, 38 | _window: u32, 39 | ) -> std::result::Result { 40 | Ok(0) 41 | } 42 | 43 | fn ratecounter_lookup_count( 44 | &mut self, 45 | _memory: &mut GuestMemory<'_>, 46 | _rc: GuestPtr, 47 | _entry: GuestPtr, 48 | _duration: u32, 49 | ) -> std::result::Result { 50 | Ok(0) 51 | } 52 | 53 | fn penaltybox_add( 54 | &mut self, 55 | _memory: &mut GuestMemory<'_>, 56 | _pb: GuestPtr, 57 | _entry: GuestPtr, 58 | _ttl: u32, 59 | ) -> std::result::Result<(), Error> { 60 | Ok(()) 61 | } 62 | 63 | fn penaltybox_has( 64 | &mut self, 65 | _memory: &mut GuestMemory<'_>, 66 | _pb: GuestPtr, 67 | _entry: GuestPtr, 68 | ) -> std::result::Result { 69 | Ok(0) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/src/wiggle_abi/fastly_purge_impl.rs: -------------------------------------------------------------------------------- 1 | //! fastly_purge` hostcall implementations. 2 | 3 | use { 4 | super::types::{PurgeOptions, PurgeOptionsMask}, 5 | crate::{error::Error, session::Session, wiggle_abi::fastly_purge::FastlyPurge}, 6 | wiggle::{GuestMemory, GuestPtr}, 7 | }; 8 | 9 | impl FastlyPurge for Session { 10 | fn purge_surrogate_key( 11 | &mut self, 12 | _memory: &mut GuestMemory<'_>, 13 | _surrogate_key: GuestPtr, 14 | _options_mask: PurgeOptionsMask, 15 | _options: GuestPtr, 16 | ) -> Result<(), Error> { 17 | Err(Error::NotAvailable("FastlyPurge")) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/wiggle_abi/geo_impl.rs: -------------------------------------------------------------------------------- 1 | //! fastly_geo` hostcall implementations. 2 | 3 | use std::{ 4 | convert::TryInto, 5 | net::{IpAddr, Ipv4Addr, Ipv6Addr}, 6 | }; 7 | 8 | use { 9 | crate::{error::Error, session::Session, wiggle_abi::fastly_geo::FastlyGeo}, 10 | std::convert::TryFrom, 11 | wiggle::{GuestMemory, GuestPtr}, 12 | }; 13 | 14 | impl FastlyGeo for Session { 15 | fn lookup( 16 | &mut self, 17 | memory: &mut GuestMemory<'_>, 18 | addr_octets: GuestPtr, 19 | addr_len: u32, 20 | buf: GuestPtr, 21 | buf_len: u32, 22 | nwritten_out: GuestPtr, 23 | ) -> Result<(), Error> { 24 | let octets = memory.to_vec(addr_octets.as_array(addr_len))?; 25 | 26 | let ip_addr: IpAddr = match addr_len { 27 | 4 => IpAddr::V4(Ipv4Addr::from( 28 | TryInto::<[u8; 4]>::try_into(octets).unwrap(), 29 | )), 30 | 16 => IpAddr::V6(Ipv6Addr::from( 31 | TryInto::<[u8; 16]>::try_into(octets).unwrap(), 32 | )), 33 | _ => return Err(Error::InvalidArgument), 34 | }; 35 | 36 | let result = self.geolocation_lookup(&ip_addr).unwrap_or_default(); 37 | 38 | if result.len() > buf_len as usize { 39 | memory.write(nwritten_out, u32::try_from(result.len()).unwrap_or(0))?; 40 | return Err(Error::BufferLengthError { 41 | buf: "geolocation_lookup", 42 | len: "geolocation_lookup_max_len", 43 | }); 44 | } 45 | 46 | let result_len = 47 | u32::try_from(result.len()).expect("smaller than value_max_len means it must fit"); 48 | 49 | memory.copy_from_slice(result.as_bytes(), buf.as_array(result_len))?; 50 | memory.write(nwritten_out, result_len)?; 51 | Ok(()) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/wiggle_abi/image_optimizer.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use crate::session::Session; 3 | use crate::wiggle_abi::{fastly_image_optimizer, types}; 4 | use wiggle::{GuestMemory, GuestPtr}; 5 | 6 | #[wiggle::async_trait] 7 | impl fastly_image_optimizer::FastlyImageOptimizer for Session { 8 | async fn transform_image_optimizer_request( 9 | &mut self, 10 | _memory: &mut GuestMemory<'_>, 11 | _origin_image_request: types::RequestHandle, 12 | _origin_image_request_body: types::BodyHandle, 13 | _origin_image_request_backend: GuestPtr, 14 | _io_transform_config_mask: types::ImageOptimizerTransformConfigOptions, 15 | _io_transform_config: GuestPtr, 16 | _io_error_detail: GuestPtr, 17 | ) -> Result<(types::ResponseHandle, types::BodyHandle), Error> { 18 | Err(Error::Unsupported { 19 | msg: "image optimizer unsupported in Viceroy", 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/wiggle_abi/log_impl.rs: -------------------------------------------------------------------------------- 1 | //! fastly_log` hostcall implementations. 2 | 3 | use { 4 | crate::{ 5 | error::Error, 6 | session::Session, 7 | wiggle_abi::{fastly_log::FastlyLog, types::EndpointHandle}, 8 | }, 9 | anyhow::anyhow, 10 | lazy_static::lazy_static, 11 | wiggle::{GuestMemory, GuestPtr}, 12 | }; 13 | 14 | fn is_reserved_endpoint(name: &[u8]) -> bool { 15 | use regex::bytes::{RegexSet, RegexSetBuilder}; 16 | const RESERVED_ENDPOINTS: &[&str] = &["^stdout$", "^stderr$", "^fst_managed_"]; 17 | lazy_static! { 18 | static ref RESERVED_ENDPOINT_RE: RegexSet = RegexSetBuilder::new(RESERVED_ENDPOINTS) 19 | .case_insensitive(true) 20 | .build() 21 | .unwrap(); 22 | } 23 | RESERVED_ENDPOINT_RE.is_match(name) 24 | } 25 | 26 | impl FastlyLog for Session { 27 | fn endpoint_get( 28 | &mut self, 29 | memory: &mut GuestMemory<'_>, 30 | name: GuestPtr<[u8]>, 31 | ) -> Result { 32 | let name = memory.as_slice(name)?.ok_or(Error::SharedMemory)?; 33 | 34 | if is_reserved_endpoint(&name) { 35 | return Err(Error::InvalidArgument); 36 | } 37 | 38 | Ok(self.log_endpoint_handle(&name)) 39 | } 40 | 41 | fn write( 42 | &mut self, 43 | memory: &mut GuestMemory<'_>, 44 | endpoint_handle: EndpointHandle, 45 | msg: GuestPtr<[u8]>, 46 | ) -> Result { 47 | let endpoint = self.log_endpoint(endpoint_handle)?; 48 | let msg = memory.as_slice(msg)?.ok_or(Error::SharedMemory)?; 49 | endpoint 50 | .write_entry(&msg) 51 | .map(|_| msg.len() as u32) 52 | .map_err(|e| Error::Other(anyhow!(e))) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/wiggle_abi/uap_impl.rs: -------------------------------------------------------------------------------- 1 | //! fastly_uap` hostcall implementations. 2 | 3 | use { 4 | crate::{error::Error, session::Session, wiggle_abi::fastly_uap::FastlyUap}, 5 | wiggle::{GuestMemory, GuestPtr}, 6 | }; 7 | 8 | impl FastlyUap for Session { 9 | fn parse( 10 | &mut self, 11 | _memory: &mut GuestMemory<'_>, 12 | _user_agent: GuestPtr, 13 | _family: GuestPtr, 14 | _family_len: u32, 15 | _family_nwritten_out: GuestPtr, 16 | _major: GuestPtr, 17 | _major_len: u32, 18 | _major_nwritten_out: GuestPtr, 19 | _minor: GuestPtr, 20 | _minor_len: u32, 21 | _minor_nwritten_out: GuestPtr, 22 | _patch: GuestPtr, 23 | _patch_len: u32, 24 | _patch_nwritten_out: GuestPtr, 25 | ) -> Result<(), Error> { 26 | Err(Error::NotAvailable("Useragent parsing")) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/wit/deps/cli/command.wit: -------------------------------------------------------------------------------- 1 | package wasi:cli@0.2.0; 2 | 3 | world command { 4 | include imports; 5 | 6 | export run; 7 | } 8 | -------------------------------------------------------------------------------- /lib/wit/deps/cli/environment.wit: -------------------------------------------------------------------------------- 1 | interface environment { 2 | /// Get the POSIX-style environment variables. 3 | /// 4 | /// Each environment variable is provided as a pair of string variable names 5 | /// and string value. 6 | /// 7 | /// Morally, these are a value import, but until value imports are available 8 | /// in the component model, this import function should return the same 9 | /// values each time it is called. 10 | get-environment: func() -> list>; 11 | 12 | /// Get the POSIX-style arguments to the program. 13 | get-arguments: func() -> list; 14 | 15 | /// Return a path that programs should use as their initial current working 16 | /// directory, interpreting `.` as shorthand for this. 17 | initial-cwd: func() -> option; 18 | } 19 | -------------------------------------------------------------------------------- /lib/wit/deps/cli/exit.wit: -------------------------------------------------------------------------------- 1 | interface exit { 2 | /// Exit the current instance and any linked instances. 3 | exit: func(status: result); 4 | } 5 | -------------------------------------------------------------------------------- /lib/wit/deps/cli/imports.wit: -------------------------------------------------------------------------------- 1 | package wasi:cli@0.2.0; 2 | 3 | world imports { 4 | include wasi:clocks/imports@0.2.0; 5 | include wasi:filesystem/imports@0.2.0; 6 | include wasi:sockets/imports@0.2.0; 7 | include wasi:random/imports@0.2.0; 8 | include wasi:io/imports@0.2.0; 9 | 10 | import environment; 11 | import exit; 12 | import stdin; 13 | import stdout; 14 | import stderr; 15 | import terminal-input; 16 | import terminal-output; 17 | import terminal-stdin; 18 | import terminal-stdout; 19 | import terminal-stderr; 20 | } 21 | -------------------------------------------------------------------------------- /lib/wit/deps/cli/run.wit: -------------------------------------------------------------------------------- 1 | interface run { 2 | /// Run the program. 3 | run: func() -> result; 4 | } 5 | -------------------------------------------------------------------------------- /lib/wit/deps/cli/stdio.wit: -------------------------------------------------------------------------------- 1 | interface stdin { 2 | use wasi:io/streams@0.2.0.{input-stream}; 3 | 4 | get-stdin: func() -> input-stream; 5 | } 6 | 7 | interface stdout { 8 | use wasi:io/streams@0.2.0.{output-stream}; 9 | 10 | get-stdout: func() -> output-stream; 11 | } 12 | 13 | interface stderr { 14 | use wasi:io/streams@0.2.0.{output-stream}; 15 | 16 | get-stderr: func() -> output-stream; 17 | } 18 | -------------------------------------------------------------------------------- /lib/wit/deps/cli/terminal.wit: -------------------------------------------------------------------------------- 1 | /// Terminal input. 2 | /// 3 | /// In the future, this may include functions for disabling echoing, 4 | /// disabling input buffering so that keyboard events are sent through 5 | /// immediately, querying supported features, and so on. 6 | interface terminal-input { 7 | /// The input side of a terminal. 8 | resource terminal-input; 9 | } 10 | 11 | /// Terminal output. 12 | /// 13 | /// In the future, this may include functions for querying the terminal 14 | /// size, being notified of terminal size changes, querying supported 15 | /// features, and so on. 16 | interface terminal-output { 17 | /// The output side of a terminal. 18 | resource terminal-output; 19 | } 20 | 21 | /// An interface providing an optional `terminal-input` for stdin as a 22 | /// link-time authority. 23 | interface terminal-stdin { 24 | use terminal-input.{terminal-input}; 25 | 26 | /// If stdin is connected to a terminal, return a `terminal-input` handle 27 | /// allowing further interaction with it. 28 | get-terminal-stdin: func() -> option; 29 | } 30 | 31 | /// An interface providing an optional `terminal-output` for stdout as a 32 | /// link-time authority. 33 | interface terminal-stdout { 34 | use terminal-output.{terminal-output}; 35 | 36 | /// If stdout is connected to a terminal, return a `terminal-output` handle 37 | /// allowing further interaction with it. 38 | get-terminal-stdout: func() -> option; 39 | } 40 | 41 | /// An interface providing an optional `terminal-output` for stderr as a 42 | /// link-time authority. 43 | interface terminal-stderr { 44 | use terminal-output.{terminal-output}; 45 | 46 | /// If stderr is connected to a terminal, return a `terminal-output` handle 47 | /// allowing further interaction with it. 48 | get-terminal-stderr: func() -> option; 49 | } 50 | -------------------------------------------------------------------------------- /lib/wit/deps/clocks/monotonic-clock.wit: -------------------------------------------------------------------------------- 1 | package wasi:clocks@0.2.0; 2 | /// WASI Monotonic Clock is a clock API intended to let users measure elapsed 3 | /// time. 4 | /// 5 | /// It is intended to be portable at least between Unix-family platforms and 6 | /// Windows. 7 | /// 8 | /// A monotonic clock is a clock which has an unspecified initial value, and 9 | /// successive reads of the clock will produce non-decreasing values. 10 | /// 11 | /// It is intended for measuring elapsed time. 12 | interface monotonic-clock { 13 | use wasi:io/poll@0.2.0.{pollable}; 14 | 15 | /// An instant in time, in nanoseconds. An instant is relative to an 16 | /// unspecified initial value, and can only be compared to instances from 17 | /// the same monotonic-clock. 18 | type instant = u64; 19 | 20 | /// A duration of time, in nanoseconds. 21 | type duration = u64; 22 | 23 | /// Read the current value of the clock. 24 | /// 25 | /// The clock is monotonic, therefore calling this function repeatedly will 26 | /// produce a sequence of non-decreasing values. 27 | now: func() -> instant; 28 | 29 | /// Query the resolution of the clock. Returns the duration of time 30 | /// corresponding to a clock tick. 31 | resolution: func() -> duration; 32 | 33 | /// Create a `pollable` which will resolve once the specified instant 34 | /// occured. 35 | subscribe-instant: func( 36 | when: instant, 37 | ) -> pollable; 38 | 39 | /// Create a `pollable` which will resolve once the given duration has 40 | /// elapsed, starting at the time at which this function was called. 41 | /// occured. 42 | subscribe-duration: func( 43 | when: duration, 44 | ) -> pollable; 45 | } 46 | -------------------------------------------------------------------------------- /lib/wit/deps/clocks/wall-clock.wit: -------------------------------------------------------------------------------- 1 | package wasi:clocks@0.2.0; 2 | /// WASI Wall Clock is a clock API intended to let users query the current 3 | /// time. The name "wall" makes an analogy to a "clock on the wall", which 4 | /// is not necessarily monotonic as it may be reset. 5 | /// 6 | /// It is intended to be portable at least between Unix-family platforms and 7 | /// Windows. 8 | /// 9 | /// A wall clock is a clock which measures the date and time according to 10 | /// some external reference. 11 | /// 12 | /// External references may be reset, so this clock is not necessarily 13 | /// monotonic, making it unsuitable for measuring elapsed time. 14 | /// 15 | /// It is intended for reporting the current date and time for humans. 16 | interface wall-clock { 17 | /// A time and date in seconds plus nanoseconds. 18 | record datetime { 19 | seconds: u64, 20 | nanoseconds: u32, 21 | } 22 | 23 | /// Read the current value of the clock. 24 | /// 25 | /// This clock is not monotonic, therefore calling this function repeatedly 26 | /// will not necessarily produce a sequence of non-decreasing values. 27 | /// 28 | /// The returned timestamps represent the number of seconds since 29 | /// 1970-01-01T00:00:00Z, also known as [POSIX's Seconds Since the Epoch], 30 | /// also known as [Unix Time]. 31 | /// 32 | /// The nanoseconds field of the output is always less than 1000000000. 33 | /// 34 | /// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16 35 | /// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time 36 | now: func() -> datetime; 37 | 38 | /// Query the resolution of the clock. 39 | /// 40 | /// The nanoseconds field of the output is always less than 1000000000. 41 | resolution: func() -> datetime; 42 | } 43 | -------------------------------------------------------------------------------- /lib/wit/deps/clocks/world.wit: -------------------------------------------------------------------------------- 1 | package wasi:clocks@0.2.0; 2 | 3 | world imports { 4 | import monotonic-clock; 5 | import wall-clock; 6 | } 7 | -------------------------------------------------------------------------------- /lib/wit/deps/filesystem/preopens.wit: -------------------------------------------------------------------------------- 1 | package wasi:filesystem@0.2.0; 2 | 3 | interface preopens { 4 | use types.{descriptor}; 5 | 6 | /// Return the set of preopened directories, and their path. 7 | get-directories: func() -> list>; 8 | } 9 | -------------------------------------------------------------------------------- /lib/wit/deps/filesystem/world.wit: -------------------------------------------------------------------------------- 1 | package wasi:filesystem@0.2.0; 2 | 3 | world imports { 4 | import types; 5 | import preopens; 6 | } 7 | -------------------------------------------------------------------------------- /lib/wit/deps/http/handler.wit: -------------------------------------------------------------------------------- 1 | /// This interface defines a handler of incoming HTTP Requests. It should 2 | /// be exported by components which can respond to HTTP Requests. 3 | interface incoming-handler { 4 | use types.{incoming-request, response-outparam}; 5 | 6 | /// This function is invoked with an incoming HTTP Request, and a resource 7 | /// `response-outparam` which provides the capability to reply with an HTTP 8 | /// Response. The response is sent by calling the `response-outparam.set` 9 | /// method, which allows execution to continue after the response has been 10 | /// sent. This enables both streaming to the response body, and performing other 11 | /// work. 12 | /// 13 | /// The implementor of this function must write a response to the 14 | /// `response-outparam` before returning, or else the caller will respond 15 | /// with an error on its behalf. 16 | handle: func( 17 | request: incoming-request, 18 | response-out: response-outparam 19 | ); 20 | } 21 | 22 | /// This interface defines a handler of outgoing HTTP Requests. It should be 23 | /// imported by components which wish to make HTTP Requests. 24 | interface outgoing-handler { 25 | use types.{ 26 | outgoing-request, request-options, future-incoming-response, error-code 27 | }; 28 | 29 | /// This function is invoked with an outgoing HTTP Request, and it returns 30 | /// a resource `future-incoming-response` which represents an HTTP Response 31 | /// which may arrive in the future. 32 | /// 33 | /// The `options` argument accepts optional parameters for the HTTP 34 | /// protocol's transport layer. 35 | /// 36 | /// This function may return an error if the `outgoing-request` is invalid 37 | /// or not allowed to be made. Otherwise, protocol errors are reported 38 | /// through the `future-incoming-response`. 39 | handle: func( 40 | request: outgoing-request, 41 | options: option 42 | ) -> result; 43 | } 44 | -------------------------------------------------------------------------------- /lib/wit/deps/http/proxy.wit: -------------------------------------------------------------------------------- 1 | package wasi:http@0.2.0; 2 | 3 | /// The `wasi:http/proxy` world captures a widely-implementable intersection of 4 | /// hosts that includes HTTP forward and reverse proxies. Components targeting 5 | /// this world may concurrently stream in and out any number of incoming and 6 | /// outgoing HTTP requests. 7 | world proxy { 8 | /// HTTP proxies have access to time and randomness. 9 | include wasi:clocks/imports@0.2.0; 10 | import wasi:random/random@0.2.0; 11 | 12 | /// Proxies have standard output and error streams which are expected to 13 | /// terminate in a developer-facing console provided by the host. 14 | import wasi:cli/stdout@0.2.0; 15 | import wasi:cli/stderr@0.2.0; 16 | 17 | /// TODO: this is a temporary workaround until component tooling is able to 18 | /// gracefully handle the absence of stdin. Hosts must return an eof stream 19 | /// for this import, which is what wasi-libc + tooling will do automatically 20 | /// when this import is properly removed. 21 | import wasi:cli/stdin@0.2.0; 22 | 23 | /// This is the default handler to use when user code simply wants to make an 24 | /// HTTP request (e.g., via `fetch()`). 25 | import outgoing-handler; 26 | 27 | /// The host delivers incoming HTTP requests to a component by calling the 28 | /// `handle` function of this exported interface. A host may arbitrarily reuse 29 | /// or not reuse component instance when delivering incoming HTTP requests and 30 | /// thus a component must be able to handle 0..N calls to `handle`. 31 | export incoming-handler; 32 | } 33 | -------------------------------------------------------------------------------- /lib/wit/deps/io/error.wit: -------------------------------------------------------------------------------- 1 | package wasi:io@0.2.0; 2 | 3 | 4 | interface error { 5 | /// A resource which represents some error information. 6 | /// 7 | /// The only method provided by this resource is `to-debug-string`, 8 | /// which provides some human-readable information about the error. 9 | /// 10 | /// In the `wasi:io` package, this resource is returned through the 11 | /// `wasi:io/streams/stream-error` type. 12 | /// 13 | /// To provide more specific error information, other interfaces may 14 | /// provide functions to further "downcast" this error into more specific 15 | /// error information. For example, `error`s returned in streams derived 16 | /// from filesystem types to be described using the filesystem's own 17 | /// error-code type, using the function 18 | /// `wasi:filesystem/types/filesystem-error-code`, which takes a parameter 19 | /// `borrow` and returns 20 | /// `option`. 21 | /// 22 | /// The set of functions which can "downcast" an `error` into a more 23 | /// concrete type is open. 24 | resource error { 25 | /// Returns a string that is suitable to assist humans in debugging 26 | /// this error. 27 | /// 28 | /// WARNING: The returned string should not be consumed mechanically! 29 | /// It may change across platforms, hosts, or other implementation 30 | /// details. Parsing this string is a major platform-compatibility 31 | /// hazard. 32 | to-debug-string: func() -> string; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/wit/deps/io/poll.wit: -------------------------------------------------------------------------------- 1 | package wasi:io@0.2.0; 2 | 3 | /// A poll API intended to let users wait for I/O events on multiple handles 4 | /// at once. 5 | interface poll { 6 | /// `pollable` represents a single I/O event which may be ready, or not. 7 | resource pollable { 8 | 9 | /// Return the readiness of a pollable. This function never blocks. 10 | /// 11 | /// Returns `true` when the pollable is ready, and `false` otherwise. 12 | ready: func() -> bool; 13 | 14 | /// `block` returns immediately if the pollable is ready, and otherwise 15 | /// blocks until ready. 16 | /// 17 | /// This function is equivalent to calling `poll.poll` on a list 18 | /// containing only this pollable. 19 | block: func(); 20 | } 21 | 22 | /// Poll for completion on a set of pollables. 23 | /// 24 | /// This function takes a list of pollables, which identify I/O sources of 25 | /// interest, and waits until one or more of the events is ready for I/O. 26 | /// 27 | /// The result `list` contains one or more indices of handles in the 28 | /// argument list that is ready for I/O. 29 | /// 30 | /// If the list contains more elements than can be indexed with a `u32` 31 | /// value, this function traps. 32 | /// 33 | /// A timeout can be implemented by adding a pollable from the 34 | /// wasi-clocks API to the list. 35 | /// 36 | /// This function does not return a `result`; polling in itself does not 37 | /// do any I/O so it doesn't fail. If any of the I/O sources identified by 38 | /// the pollables has an error, it is indicated by marking the source as 39 | /// being reaedy for I/O. 40 | poll: func(in: list>) -> list; 41 | } 42 | -------------------------------------------------------------------------------- /lib/wit/deps/io/world.wit: -------------------------------------------------------------------------------- 1 | package wasi:io@0.2.0; 2 | 3 | world imports { 4 | import streams; 5 | import poll; 6 | } 7 | -------------------------------------------------------------------------------- /lib/wit/deps/random/insecure-seed.wit: -------------------------------------------------------------------------------- 1 | package wasi:random@0.2.0; 2 | /// The insecure-seed interface for seeding hash-map DoS resistance. 3 | /// 4 | /// It is intended to be portable at least between Unix-family platforms and 5 | /// Windows. 6 | interface insecure-seed { 7 | /// Return a 128-bit value that may contain a pseudo-random value. 8 | /// 9 | /// The returned value is not required to be computed from a CSPRNG, and may 10 | /// even be entirely deterministic. Host implementations are encouraged to 11 | /// provide pseudo-random values to any program exposed to 12 | /// attacker-controlled content, to enable DoS protection built into many 13 | /// languages' hash-map implementations. 14 | /// 15 | /// This function is intended to only be called once, by a source language 16 | /// to initialize Denial Of Service (DoS) protection in its hash-map 17 | /// implementation. 18 | /// 19 | /// # Expected future evolution 20 | /// 21 | /// This will likely be changed to a value import, to prevent it from being 22 | /// called multiple times and potentially used for purposes other than DoS 23 | /// protection. 24 | insecure-seed: func() -> tuple; 25 | } 26 | -------------------------------------------------------------------------------- /lib/wit/deps/random/insecure.wit: -------------------------------------------------------------------------------- 1 | package wasi:random@0.2.0; 2 | /// The insecure interface for insecure pseudo-random numbers. 3 | /// 4 | /// It is intended to be portable at least between Unix-family platforms and 5 | /// Windows. 6 | interface insecure { 7 | /// Return `len` insecure pseudo-random bytes. 8 | /// 9 | /// This function is not cryptographically secure. Do not use it for 10 | /// anything related to security. 11 | /// 12 | /// There are no requirements on the values of the returned bytes, however 13 | /// implementations are encouraged to return evenly distributed values with 14 | /// a long period. 15 | get-insecure-random-bytes: func(len: u64) -> list; 16 | 17 | /// Return an insecure pseudo-random `u64` value. 18 | /// 19 | /// This function returns the same type of pseudo-random data as 20 | /// `get-insecure-random-bytes`, represented as a `u64`. 21 | get-insecure-random-u64: func() -> u64; 22 | } 23 | -------------------------------------------------------------------------------- /lib/wit/deps/random/random.wit: -------------------------------------------------------------------------------- 1 | package wasi:random@0.2.0; 2 | /// WASI Random is a random data API. 3 | /// 4 | /// It is intended to be portable at least between Unix-family platforms and 5 | /// Windows. 6 | interface random { 7 | /// Return `len` cryptographically-secure random or pseudo-random bytes. 8 | /// 9 | /// This function must produce data at least as cryptographically secure and 10 | /// fast as an adequately seeded cryptographically-secure pseudo-random 11 | /// number generator (CSPRNG). It must not block, from the perspective of 12 | /// the calling program, under any circumstances, including on the first 13 | /// request and on requests for numbers of bytes. The returned data must 14 | /// always be unpredictable. 15 | /// 16 | /// This function must always return fresh data. Deterministic environments 17 | /// must omit this function, rather than implementing it with deterministic 18 | /// data. 19 | get-random-bytes: func(len: u64) -> list; 20 | 21 | /// Return a cryptographically-secure random or pseudo-random `u64` value. 22 | /// 23 | /// This function returns the same type of data as `get-random-bytes`, 24 | /// represented as a `u64`. 25 | get-random-u64: func() -> u64; 26 | } 27 | -------------------------------------------------------------------------------- /lib/wit/deps/random/world.wit: -------------------------------------------------------------------------------- 1 | package wasi:random@0.2.0; 2 | 3 | world imports { 4 | import random; 5 | import insecure; 6 | import insecure-seed; 7 | } 8 | -------------------------------------------------------------------------------- /lib/wit/deps/sockets/instance-network.wit: -------------------------------------------------------------------------------- 1 | 2 | /// This interface provides a value-export of the default network handle.. 3 | interface instance-network { 4 | use network.{network}; 5 | 6 | /// Get a handle to the default network. 7 | instance-network: func() -> network; 8 | 9 | } 10 | -------------------------------------------------------------------------------- /lib/wit/deps/sockets/ip-name-lookup.wit: -------------------------------------------------------------------------------- 1 | 2 | interface ip-name-lookup { 3 | use wasi:io/poll@0.2.0.{pollable}; 4 | use network.{network, error-code, ip-address}; 5 | 6 | 7 | /// Resolve an internet host name to a list of IP addresses. 8 | /// 9 | /// Unicode domain names are automatically converted to ASCII using IDNA encoding. 10 | /// If the input is an IP address string, the address is parsed and returned 11 | /// as-is without making any external requests. 12 | /// 13 | /// See the wasi-socket proposal README.md for a comparison with getaddrinfo. 14 | /// 15 | /// This function never blocks. It either immediately fails or immediately 16 | /// returns successfully with a `resolve-address-stream` that can be used 17 | /// to (asynchronously) fetch the results. 18 | /// 19 | /// # Typical errors 20 | /// - `invalid-argument`: `name` is a syntactically invalid domain name or IP address. 21 | /// 22 | /// # References: 23 | /// - 24 | /// - 25 | /// - 26 | /// - 27 | resolve-addresses: func(network: borrow, name: string) -> result; 28 | 29 | resource resolve-address-stream { 30 | /// Returns the next address from the resolver. 31 | /// 32 | /// This function should be called multiple times. On each call, it will 33 | /// return the next address in connection order preference. If all 34 | /// addresses have been exhausted, this function returns `none`. 35 | /// 36 | /// This function never returns IPv4-mapped IPv6 addresses. 37 | /// 38 | /// # Typical errors 39 | /// - `name-unresolvable`: Name does not exist or has no suitable associated IP addresses. (EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY) 40 | /// - `temporary-resolver-failure`: A temporary failure in name resolution occurred. (EAI_AGAIN) 41 | /// - `permanent-resolver-failure`: A permanent failure in name resolution occurred. (EAI_FAIL) 42 | /// - `would-block`: A result is not available yet. (EWOULDBLOCK, EAGAIN) 43 | resolve-next-address: func() -> result, error-code>; 44 | 45 | /// Create a `pollable` which will resolve once the stream is ready for I/O. 46 | /// 47 | /// Note: this function is here for WASI Preview2 only. 48 | /// It's planned to be removed when `future` is natively supported in Preview3. 49 | subscribe: func() -> pollable; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/wit/deps/sockets/tcp-create-socket.wit: -------------------------------------------------------------------------------- 1 | 2 | interface tcp-create-socket { 3 | use network.{network, error-code, ip-address-family}; 4 | use tcp.{tcp-socket}; 5 | 6 | /// Create a new TCP socket. 7 | /// 8 | /// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` in POSIX. 9 | /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. 10 | /// 11 | /// This function does not require a network capability handle. This is considered to be safe because 12 | /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind`/`connect` 13 | /// is called, the socket is effectively an in-memory configuration object, unable to communicate with the outside world. 14 | /// 15 | /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. 16 | /// 17 | /// # Typical errors 18 | /// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) 19 | /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) 20 | /// 21 | /// # References 22 | /// - 23 | /// - 24 | /// - 25 | /// - 26 | create-tcp-socket: func(address-family: ip-address-family) -> result; 27 | } 28 | -------------------------------------------------------------------------------- /lib/wit/deps/sockets/udp-create-socket.wit: -------------------------------------------------------------------------------- 1 | 2 | interface udp-create-socket { 3 | use network.{network, error-code, ip-address-family}; 4 | use udp.{udp-socket}; 5 | 6 | /// Create a new UDP socket. 7 | /// 8 | /// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` in POSIX. 9 | /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. 10 | /// 11 | /// This function does not require a network capability handle. This is considered to be safe because 12 | /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind` is called, 13 | /// the socket is effectively an in-memory configuration object, unable to communicate with the outside world. 14 | /// 15 | /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. 16 | /// 17 | /// # Typical errors 18 | /// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) 19 | /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) 20 | /// 21 | /// # References: 22 | /// - 23 | /// - 24 | /// - 25 | /// - 26 | create-udp-socket: func(address-family: ip-address-family) -> result; 27 | } 28 | -------------------------------------------------------------------------------- /lib/wit/deps/sockets/world.wit: -------------------------------------------------------------------------------- 1 | package wasi:sockets@0.2.0; 2 | 3 | world imports { 4 | import instance-network; 5 | import network; 6 | import udp; 7 | import udp-create-socket; 8 | import tcp; 9 | import tcp-create-socket; 10 | import ip-name-lookup; 11 | } 12 | -------------------------------------------------------------------------------- /lib/wit/viceroy.wit: -------------------------------------------------------------------------------- 1 | package fastly:viceroy; 2 | 3 | world compute { 4 | include fastly:api/compute; 5 | } -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | 1.83 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Viceroy uses the default formatting settings. 2 | -------------------------------------------------------------------------------- /test-fixtures/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-fixtures" 3 | version = "0.1.0" 4 | description = "Guest wasm programs intended for integration testing." 5 | authors = ["Fastly"] 6 | edition = "2021" 7 | license = "Apache-2.0 WITH LLVM-exception" 8 | publish = false 9 | 10 | [dependencies] 11 | anyhow = "1.0.86" 12 | base64 = "0.21.2" 13 | bitflags = "1.3.2" 14 | fastly = "0.11.1" 15 | fastly-shared = "0.11.1" 16 | fastly-sys = "0.11.1" 17 | hex-literal = "0.4.1" 18 | bytes = "1.0.0" 19 | http = "1.1.0" 20 | rustls-pemfile = "1.0.3" 21 | serde = "1.0.114" 22 | sha2 = "0.10.8" 23 | uuid = { version = "1.15.1", features = ["v4"] } 24 | -------------------------------------------------------------------------------- /test-fixtures/data/ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,6403E194E2FD15C4 4 | 5 | JXGAIQ6/3eVUy7R7PulfsnuNfTQye+HOw9EfLwsz6Tm6AxEiQkkXLyVP7BrmPRfa 6 | J1WCngcSwyp1GpOD8qfQOaM0dxS+/bvVvB4eocTxgkDB42MG2HRZpJRrWGQ1VxUT 7 | vIdlV/zTyjC7aAQj5tTJkKN11+lwFxpcvlb3fNbQB23xWdi8yYGTHrEBw6cl5Szi 8 | ipByvcbreKVH4eGrJLcC/H4wyrCe7CwzbP8deHNsH5WrxEWgkkjJMBAk6rzyp9su 9 | F6FNkJLup4Cfk7gmkpQsXs0Y+8LVnBfhc3dQrxdXZjVccFppvRDnoa9uzMQuS56U 10 | jP+wloYeEVS7hfualKJFMNoC3oB7sI6PQGgYi047EkTEIBFFv1ryBcUAwzSlNdoe 11 | 2MF1b4OpXrkR0J718QSE/ZpXe4K5HFtRtaE5b6Y47PbMoE3q+ldONEK2jLTvPtIv 12 | DOZWk8H67U0TOM53UQO5fVAntmPPuEiCX2cm0rISF7dwlgYG1obhdko3vir/6cJw 13 | Cg9WbF5dGZYUUbZeNVdXHT+b7MaQOskXXBf+gUVRX/KIRLLP+yH1T06zxzYNHuSD 14 | JBkaklRuXBZnyRPTrqaFNmfgo9XoDY4Yd1QeKfD6Os5bvZeqBYgj3S+vmRUYZeE+ 15 | PYKjzhIMjNmMP7ZP+h0iYs2rt6bRCgjTsd15ZjalPm+0f702sS51G1C8mdwy5ScQ 16 | jX+1PTgpcfZ9aWZLmabC1Cvg1wOH0foAzbtag9qH/FUZgaLP0lfDhlAHCJwOrpRd 17 | 3kegjfI1SXE2NGGq5GULU3bP50VYlWxpQNgWVImLnnpTCANfIV6QBx7JVVIxAJps 18 | LtzSt3fjU5wjNDE/GbwzzzA1DxissZzrXM2Q+ac69qPOE38e0PDsRahh2qf+1Db9 19 | ozUvKOMbk8PAWUr5dtp9SFYgaF6gGoQY5JswP9tqHjTLYYYXZR21auZSIQXiG2yC 20 | wPBsSpnscBQSzo21+95f3jvrnq3HUFwcU5jE1W+RVhCQAQi0ZQdsYTyTsB5OksS3 21 | hXNBWFTVIOJjyUa0nwsh3hnLKwvl0/Sb8n5qssRMrx6ltN/JadA0I7dBHvjMr9n4 22 | Z1LkXQSKiGePcbzE8X4WhGApx6LwQgQm8OMqB3vw+HsGRmmzvpuy54mZgl8SV6ob 23 | R62uMVbuvNTJAr4XpGlsiZ+2FWKysvIXj5VEGwSfEHw8PIOuFgtJSS455iEokdid 24 | JJ9tfenJOCVBKLN5dYxzKbsOEvPdXIal9mM9HCPIqpIMhHRAzV1d47KUIrP6i8b8 25 | O8SXUZJz+JYvCUw6VYOorA7QRx8KBmZVRlJ3h5mocQ3BQIExu+XRQ8SVSYu/aedh 26 | zkDn9F0jg+LGLh10hh+L97glWWw48uFVFjMXUDhrcn2bA1xyvK0WNW8fnZ2pxjSj 27 | ArDxC1qLiH0dRDAzXYOktmm7ToJppz7ncXnRAA7C9ZbGxj8w9D43Ypew+JBPjBKm 28 | XoXNsZLd8j7v66IJfKavA8+wV8u9GKsy6XaCX3y4/qzeMKiElii1bqcLz+Xvo3ZS 29 | kFPwN016SrFfF6jfwIaknZr2u9QELa5B5EIUsGC3PCwGUsqDNTkP3g== 30 | -----END RSA PRIVATE KEY----- 31 | -------------------------------------------------------------------------------- /test-fixtures/data/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDqTCCApGgAwIBAgIUDXDr/2fouphqlB8iJASenWOr/XwwDQYJKoZIhvcNAQEL 3 | BQAwZDELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk9yZWdvbjERMA8GA1UEBwwIUG9y 4 | dGxhbmQxEDAOBgNVBAoMB1ZpY2Vyb3kxHzAdBgkqhkiG9w0BCQEWEGF3aWNrQGZh 5 | c3RseS5jb20wHhcNMjMwNzI3MDAwODU5WhcNMzMwNzI0MDAwODU5WjBkMQswCQYD 6 | VQQGEwJVUzEPMA0GA1UECAwGT3JlZ29uMREwDwYDVQQHDAhQb3J0bGFuZDEQMA4G 7 | A1UECgwHVmljZXJveTEfMB0GCSqGSIb3DQEJARYQYXdpY2tAZmFzdGx5LmNvbTCC 8 | ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKxXdG4C6yEeLTtFPOXWTv1N 9 | eEeJMLcAoupB9u3x0PYT+w+0ruAympviqGbEiyZL/qMKLYenLiQO+72VCISW5qfB 10 | ZoCpwDxBon5TDUZ98JU93nVRml7uOg25G+KTs3aeJt6+rFDPNaNyxVcKgCuURB4y 11 | mwgosLUvxoEffFnHlURETLN4aSGQ6TLp8YEJp4EudTVo/l+kdhm6sLZMBkmUxnnl 12 | muEc8ePAr1igYchz2tbcWRjzxoUOuEdoKaW2OCElNObt2WYPWzHs+6p1K8+KyTRY 13 | /pVOFtA43nuWmk++UHFthBAw9IqBuO0FMJr4SULnKfiTh5E9F+nZ0Q/1nfzzsAMC 14 | AwEAAaNTMFEwHQYDVR0OBBYEFGYM6HhP8yZ17eXw5nOfQ971u1l9MB8GA1UdIwQY 15 | MBaAFGYM6HhP8yZ17eXw5nOfQ971u1l9MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI 16 | hvcNAQELBQADggEBAFmFkUodKTXeT683GEj4SoiMbDL8d3x+Vc+kvLPC2Jloru4R 17 | Qo0USu3eJZjNxKjmPbLii8gzf5ZmZHdytWQ+5irYjXBHrE9tPgmpavhM+0otpnUd 18 | vYosnfwv/aQEIiqeMkpqzbSKvb2I+TVpAC1xb6qbYE95tnsX/KEdAoJ/SAcZLGYQ 19 | LKGTjz3eKlgUWy69uwzHXkie8hxDVRlyA7cFY4AAqsLhL2KQPWtMT7fRKrVKfLYd 20 | Qq7tJAMLnPnAdAUousI0RDcLpB8adGkhZH66lL4oV9U+aQ0dA0oiqSKZtMoHeWbr 21 | /L0ti7ZOfxOxRRCzt8KdLo/kGNTfAz+74P0MY80= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /test-fixtures/data/ca.srl: -------------------------------------------------------------------------------- 1 | 3A9F7B82F32561D05823FDF2AE90DE1DB771E518 2 | -------------------------------------------------------------------------------- /test-fixtures/data/client.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDvjCCAqagAwIBAgIUOp97gvMlYdBYI/3yrpDeHbdx5RgwDQYJKoZIhvcNAQEL 3 | BQAwZDELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk9yZWdvbjERMA8GA1UEBwwIUG9y 4 | dGxhbmQxEDAOBgNVBAoMB1ZpY2Vyb3kxHzAdBgkqhkiG9w0BCQEWEGF3aWNrQGZh 5 | c3RseS5jb20wHhcNMjMwNzI3MDAxOTU0WhcNMzMwNzI0MDAxOTU0WjB1MQswCQYD 6 | VQQGEwJVUzEPMA0GA1UECAwGT3JlZ29uMREwDwYDVQQHDAhQb3J0bGFuZDEQMA4G 7 | A1UECgwHVmljZXJveTEPMA0GA1UECwwGQ2xpZW50MR8wHQYJKoZIhvcNAQkBFhBh 8 | d2lja0BmYXN0bHkuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA 9 | z27x1GpD46K6b9/3PNyZYKgTL9GBbpLAVF8Uebd34ftUfnWZ3ER+x6A1YbacHnL1 10 | 12diPPevyYkpXuiujwCeswYNrZHEtiRfAvrzBRhnhL8owQTxjOcG4EOzR7Je556F 11 | Tq8kNth5iHckORjmXiV9ZahbLv/zBFpkXpDeze62zd8y9chPNEqcrLZBOb4UoKXm 12 | Ot1lIdeo23nysR4rC6XemWNSFcZv9zagUzliMeca3XN2RIUAFZv4o+gYPqqXQi+0 13 | a+OOq0jnKpawW+avn2UG7wzXGlLcVOvLe5BOCA1RfWtR8w03MFdvoBAesXJ4xGX1 14 | ROUzelldedmpqtvORdhmGQIDAQABo1cwVTAfBgNVHSMEGDAWgBRmDOh4T/Mmde3l 15 | 8OZzn0Pe9btZfTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE8DAaBgNVHREEEzARggls 16 | b2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcNAQELBQADggEBAJ84GzmmqsmmtqXcmZIH 17 | i644p8wIc/DXPqb7zzAVm9FXpFgW3mN4xu1JYWu+rb1sge8uIm7Vt5Isd4CZ89XI 18 | F2Q2DS/rKMQmjgSDReWm9G+qZROwuhNDzK85e73Rw2EdX6cXtAGR1h3IdOTIv1FC 19 | UElFER31U8i4J9pxUZF/FTzlPEA1agqMsO6hQlj/A9B6TtzL7SSxCFBBaFbNCLMC 20 | D/WCrIoklNV5TwutYG80EYZhJlfUJPDQBphkcetDBI0L/KL/n20bg8OR/epGD5++ 21 | qKIulxf9iUR5QHm2fWKdTLOuADmV+lc925gIqGhFhjVvpNPOcdckecQUp3vCNu2/ 22 | HrM= 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /test-fixtures/data/client.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICujCCAaICAQAwdTELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk9yZWdvbjERMA8G 3 | A1UEBwwIUG9ydGxhbmQxEDAOBgNVBAoMB1ZpY2Vyb3kxDzANBgNVBAsMBkNsaWVu 4 | dDEfMB0GCSqGSIb3DQEJARYQYXdpY2tAZmFzdGx5LmNvbTCCASIwDQYJKoZIhvcN 5 | AQEBBQADggEPADCCAQoCggEBAM9u8dRqQ+Oium/f9zzcmWCoEy/RgW6SwFRfFHm3 6 | d+H7VH51mdxEfsegNWG2nB5y9ddnYjz3r8mJKV7oro8AnrMGDa2RxLYkXwL68wUY 7 | Z4S/KMEE8YznBuBDs0eyXueehU6vJDbYeYh3JDkY5l4lfWWoWy7/8wRaZF6Q3s3u 8 | ts3fMvXITzRKnKy2QTm+FKCl5jrdZSHXqNt58rEeKwul3pljUhXGb/c2oFM5YjHn 9 | Gt1zdkSFABWb+KPoGD6ql0IvtGvjjqtI5yqWsFvmr59lBu8M1xpS3FTry3uQTggN 10 | UX1rUfMNNzBXb6AQHrFyeMRl9UTlM3pZXXnZqarbzkXYZhkCAwEAAaAAMA0GCSqG 11 | SIb3DQEBCwUAA4IBAQAAU/iqM3cx76vLFcV18xnnpi7jQjkxTvP5uq/7gisChgv1 12 | Z26DS6ZYihlFKJYt0fFXCh9CG61+ttkw6Cz53G62+iV/C3+0tamn4G8EQddZshg7 13 | QpRousktgfJKHIIQLfzqb6fe8G49jmQlAjdseenBn1dHndxRLNOtM2c/smaGaafU 14 | yTdPCdLDleqfCcUMvMjGp/WYo9j33Mw4ZN1C9IUjFc0+3Ythsw0hrZmOkr0X90v4 15 | H77tZsS+ySWbeInD7Ku6IJzDimYAlXNG251LFdgSdX0dkxCfTmhFf9Z6Gl/jNufO 16 | ZEiHpX9oR7tFq89Ww6eMgW8gKJoyH9+1N91PtxCo 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /test-fixtures/data/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAz27x1GpD46K6b9/3PNyZYKgTL9GBbpLAVF8Uebd34ftUfnWZ 3 | 3ER+x6A1YbacHnL112diPPevyYkpXuiujwCeswYNrZHEtiRfAvrzBRhnhL8owQTx 4 | jOcG4EOzR7Je556FTq8kNth5iHckORjmXiV9ZahbLv/zBFpkXpDeze62zd8y9chP 5 | NEqcrLZBOb4UoKXmOt1lIdeo23nysR4rC6XemWNSFcZv9zagUzliMeca3XN2RIUA 6 | FZv4o+gYPqqXQi+0a+OOq0jnKpawW+avn2UG7wzXGlLcVOvLe5BOCA1RfWtR8w03 7 | MFdvoBAesXJ4xGX1ROUzelldedmpqtvORdhmGQIDAQABAoIBAQCsbu6KhDehMDHJ 8 | NCWjK0I4zh78/iyZDVbiDBPKRpBag4GuifX329yD95LIgnNvAGOKxz8rrT4sy19f 9 | rQ8Ggx5pdVvDcExUmRF+Obvw/WN4PywSoBhn59iYbs7Gh+lKo0Tvrrns+bC1l0y+ 10 | RguiMYn3CqeZ/1w1vyp2TflYuNqvcR4zMzJ4dN474CCLPIUX9OfK21Lbv/UMdguF 11 | Rs/BuStucqaCzEtTLyZYlxQc1i8S8Uy2yukXR6TYWJOsWZj0KIgH/YI7ZgzvTIxL 12 | ax4Hn4jIHPFSJ+vl2ehDKffkQQ0lzm60ASkjaJY6GsFoTQzsmuafpLIAoJbDbZR1 13 | txPSFC+BAoGBAPbp6+LsXoEY+4RfStg4c/oLWmK3aTxzQzMY90vxnMm6SJTwTPAm 14 | pO+Pp2UGyEGHV7hg3d+ItWpM9QGVmsjm+punIfc0W/0+AVUonjPLfv44dz7+geYt 15 | /oeMv4RTqCclROvtQTqV6hHn4E3Xg061miEe6OxYmqfZuLD2nv2VlsQRAoGBANcR 16 | GAqeClQtraTnu+yU9U+FJZfvSxs1yHr7XItCMtwxeU6+nipa+3pXNnKu0dKKekUG 17 | PCdUipXgggA6OUm2YFKPUhiXJUNoHCj45Tkv2NshGplW33U3NcCkDqL7vvZoBBfP 18 | OPxEVRVEIlwp/WzEambs9MjWoecEaOe7/3UCVumJAoGANlfVquQLCK7O7JtshZon 19 | LGlDQ2bKqptTtvNPuk87CssNHnqk9FYNBwy+8uVDPejjzZjEPGaCRxsY8XhT0NPF 20 | ZGysdRP5CwuSj4OZDh1DngAffqXVQSvuUTcRD7a506PIP4TATnygP8ChBYDhTXl6 21 | qr961EnMABVTKN+eroE15YECgYEAv+YLyqV71+KuNx9i6lV7kcnfYnNtU8koqruQ 22 | tt2Jnjoy4JVrcaWfEGmzNp9Qr4lKUj6e/AUOZ29c8DEDnwcxaVliynhLEptZzSFQ 23 | /zb3S4d9QWdnmiJ6Pvrj6H+yxBDJ3ijT0xxxwrj547y/2QZlXpN+U5pX+ldP974i 24 | 0dgVjukCgYEArxv0dO2VEguWLx5YijHiN72nDDI+skbfkQkvWQjA7x8R9Xx1SWUl 25 | WeyeaaV5rqfJZF1wBCK5VJndjbOGhPh6u/0mpeYw4Ty3+CKN2WoikQO27qYfMZW5 26 | vvT7m9ZR+gkm2TjZ+pZuilz2gqu/yMJKl8Fi8Q7dsb8eWedWQXjbUZg= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test-fixtures/data/device-detection-mapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0 [FBAN/FBIOS;FBAV/8.0.0.28.18;FBBV/1665515;FBDV/iPhone4,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/7.0.4;FBSS/2; FBCR/Telekom.de;FBID/phone;FBLC/de_DE;FBOP/5]": { 3 | "user_agent": {}, 4 | "os": {}, 5 | "device": { 6 | "name": "iPhone", 7 | "brand": "Apple", 8 | "model": "iPhone4,1", 9 | "hwtype": "Mobile Phone", 10 | "is_ereader": false, 11 | "is_gameconsole": false, 12 | "is_mediaplayer": false, 13 | "is_mobile": true, 14 | "is_smarttv": false, 15 | "is_tablet": false, 16 | "is_tvplayer": false, 17 | "is_desktop": false, 18 | "is_touchscreen": true 19 | } 20 | }, 21 | "ghosts-app/1.0.2.1 (ASUSTeK COMPUTER INC.; X550CC; Windows 8 (X86); en)": { 22 | "user_agent": {}, 23 | "os": {}, 24 | "device": { 25 | "name": "Asus TeK", 26 | "brand": "Asus", 27 | "model": "TeK", 28 | "is_desktop": false 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /test-fixtures/data/geolocation-mapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "127.0.0.1": { 3 | "as_name": "Fastly Test", 4 | "as_number": 12345, 5 | "area_code": 123, 6 | "city": "Test City", 7 | "conn_speed": "broadband", 8 | "conn_type": "wired", 9 | "continent": "NA", 10 | "country_code": "CA", 11 | "country_code3": "CAN", 12 | "country_name": "Canada", 13 | "latitude": 12.345, 14 | "longitude": 54.321, 15 | "metro_code": 0, 16 | "postal_code": "12345", 17 | "proxy_description": "?", 18 | "proxy_type": "?", 19 | "region": "BC", 20 | "utc_offset": -700 21 | }, 22 | "0000:0000:0000:0000:0000:0000:0000:0001": { 23 | "as_name": "Fastly Test IPv6", 24 | "as_number": 12345, 25 | "area_code": 123, 26 | "city": "Test City IPv6", 27 | "conn_speed": "broadband", 28 | "conn_type": "wired", 29 | "continent": "NA", 30 | "country_code": "CA", 31 | "country_code3": "CAN", 32 | "country_name": "Canada", 33 | "latitude": 12.345, 34 | "longitude": 54.321, 35 | "metro_code": 0, 36 | "postal_code": "12345", 37 | "proxy_description": "?", 38 | "proxy_type": "?", 39 | "region": "BC", 40 | "utc_offset": -700 41 | } 42 | } -------------------------------------------------------------------------------- /test-fixtures/data/hello_world.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastly/Viceroy/2b7227e19220ed072c525be80e7c4406ece5aba9/test-fixtures/data/hello_world.gz -------------------------------------------------------------------------------- /test-fixtures/data/json-config_store.json: -------------------------------------------------------------------------------- 1 | { 2 | "cat": "meow", 3 | "dog": "woof" 4 | } 5 | -------------------------------------------------------------------------------- /test-fixtures/data/json-dictionary.json: -------------------------------------------------------------------------------- 1 | { 2 | "cat": "meow", 3 | "dog": "woof" 4 | } 5 | -------------------------------------------------------------------------------- /test-fixtures/data/json-kv_store-invalid_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "first": { 3 | } 4 | } 5 | -------------------------------------------------------------------------------- /test-fixtures/data/json-kv_store-invalid_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "first": { 3 | "data": "this is invalid because it has both data and file", 4 | "file": "../test-fixtures/data/kv-store.txt" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/data/json-kv_store.json: -------------------------------------------------------------------------------- 1 | { 2 | "first": "This is some data", 3 | "second": { 4 | "file": "../test-fixtures/data/kv-store.txt" 5 | }, 6 | "third": { 7 | "data": "third", 8 | "metadata": "some metadata" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test-fixtures/data/json-secret_store.json: -------------------------------------------------------------------------------- 1 | { 2 | "first": "first secret", 3 | "second": "another secret" 4 | } 5 | -------------------------------------------------------------------------------- /test-fixtures/data/kv-store.txt: -------------------------------------------------------------------------------- 1 | More data -------------------------------------------------------------------------------- /test-fixtures/data/my-acl-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": [ 3 | { "op": "update", "prefix": "1.2.3.0/24", "action": "BLOCK" }, 4 | { "op": "create", "prefix": "192.168.0.0/16", "action": "BLOCK" }, 5 | { "op": "update", "prefix": "23.23.23.23/32", "action": "ALLOW" }, 6 | { "op": "create", "prefix": "1.2.3.4/32", "action": "ALLOW" } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test-fixtures/data/my-acl-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": [ 3 | { "op": "update", "prefix": "2000::/24", "action": "BLOCK" }, 4 | { "op": "create", "prefix": "FACE::/16", "action": "ALLOW" } 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/data/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDvjCCAqagAwIBAgIUOp97gvMlYdBYI/3yrpDeHbdx5RcwDQYJKoZIhvcNAQEL 3 | BQAwZDELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk9yZWdvbjERMA8GA1UEBwwIUG9y 4 | dGxhbmQxEDAOBgNVBAoMB1ZpY2Vyb3kxHzAdBgkqhkiG9w0BCQEWEGF3aWNrQGZh 5 | c3RseS5jb20wHhcNMjMwNzI3MDAxODA5WhcNMzMwNzI0MDAxODA5WjB1MQswCQYD 6 | VQQGEwJVUzEPMA0GA1UECAwGT3JlZ29uMREwDwYDVQQHDAhQb3J0bGFuZDEQMA4G 7 | A1UECgwHVmljZXJveTEPMA0GA1UECwwGU2VydmVyMR8wHQYJKoZIhvcNAQkBFhBh 8 | d2lja0BmYXN0bHkuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA 9 | vaJbeui08EYihCfktu3Ary6CnxNmsg8kCXPddRdGa9+QJGGkSNBf4AseYI/jXPpO 10 | /eMixAnlv5ebie9vTazKPNxXwDOzoFu3Gt8jFKwI0jsmkcIIk1wC0CDQrsAm50uC 11 | la6zEGWLF/Fd5lfoXZ5Jo20LgHV7t9tXsZCzT3RgC0dKkrnGumJK5g1OYGN6l4wh 12 | i2zOCMvIT/V5Am7epCZx1GS6O2mVLZhrfgpfFiRx6o1QV07XXnW+LFz8Rk5cwOUl 13 | t4K9h0DSc9p5yPvgz3DDw2qA4rcr7BK18ePUJR1khey8aDQWzHfSfVj8OwLlhLN8 14 | ixdXv+YLvGKaOsVH2cvt2wIDAQABo1cwVTAfBgNVHSMEGDAWgBRmDOh4T/Mmde3l 15 | 8OZzn0Pe9btZfTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE8DAaBgNVHREEEzARggls 16 | b2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcNAQELBQADggEBAFE8Kycq90Dnij9WJx5n 17 | BuR+KU/o4nV3NjVHmo503KxIW6PFuOMSc65Uxq0Q6IZmJvzovdnCRRda+n9c/1dV 18 | APFTwG5QCOJOEzK7SgJpvxlkZ+39fMBPpakcBOUobFqifaltKum5x3pT9zn1n2jx 19 | EGay3RWT45wXtKBEd0zZ/vMu0fFGoMBzT696ywVMzbSI7o745UN3fwrE8FVGkJXk 20 | V5F+6ugjXpqhqPi3rZRfC+ZJolFe+blj5wlXScwg/W5BKQDbotCZFCswk00J++/M 21 | 516yQItLQUU4lYJzVAqlrHIwJeBhAinHlpfNRqQJtxVE2VIzlMRp4n2VxZO64IfH 22 | xaw= 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /test-fixtures/data/server.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICujCCAaICAQAwdTELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk9yZWdvbjERMA8G 3 | A1UEBwwIUG9ydGxhbmQxEDAOBgNVBAoMB1ZpY2Vyb3kxDzANBgNVBAsMBlNlcnZl 4 | cjEfMB0GCSqGSIb3DQEJARYQYXdpY2tAZmFzdGx5LmNvbTCCASIwDQYJKoZIhvcN 5 | AQEBBQADggEPADCCAQoCggEBAL2iW3rotPBGIoQn5LbtwK8ugp8TZrIPJAlz3XUX 6 | RmvfkCRhpEjQX+ALHmCP41z6Tv3jIsQJ5b+Xm4nvb02syjzcV8Azs6BbtxrfIxSs 7 | CNI7JpHCCJNcAtAg0K7AJudLgpWusxBlixfxXeZX6F2eSaNtC4B1e7fbV7GQs090 8 | YAtHSpK5xrpiSuYNTmBjepeMIYtszgjLyE/1eQJu3qQmcdRkujtplS2Ya34KXxYk 9 | ceqNUFdO1151vixc/EZOXMDlJbeCvYdA0nPaecj74M9ww8NqgOK3K+wStfHj1CUd 10 | ZIXsvGg0Fsx30n1Y/DsC5YSzfIsXV7/mC7ximjrFR9nL7dsCAwEAAaAAMA0GCSqG 11 | SIb3DQEBCwUAA4IBAQBza1mDXtkyCeUbQzaNW0QamUWmGTijIAP6klE4w2Wy0cN5 12 | XrkkRQXMRGDfwOnkTqsv8p1AHKjPdnC2fY4a4fn5qR4/6dBVB40889UH39w1qcDW 13 | L+ybOPDA/UjhiXoL2EiW5iLyHeK/ttHFgvq5niHUmYk/ZVElS4xD0FAIIagxDa/u 14 | zTXYwlf+ZT4KdjPdx/s9evRQQWrS5Z06Rv9Lzkl5A9WCUXgyGw16MvU0rA/SFEgu 15 | 98XqUY7wa5DC7RuW38IKk06CGBDWNc3l1ZxQ+VB/sym702nbV2HFZT46viflll1a 16 | FjgQEu8ztbDfgooRGGHimZZXA2dBEHirgdM0bdzy 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /test-fixtures/data/server.ext: -------------------------------------------------------------------------------- 1 | authorityKeyIdentifier=keyid,issuer 2 | basicConstraints=CA:FALSE 3 | keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment 4 | subjectAltName = @alt_names 5 | 6 | [alt_names] 7 | DNS.1 = localhost 8 | IP.1 = 127.0.0.1 9 | -------------------------------------------------------------------------------- /test-fixtures/data/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEAvaJbeui08EYihCfktu3Ary6CnxNmsg8kCXPddRdGa9+QJGGk 3 | SNBf4AseYI/jXPpO/eMixAnlv5ebie9vTazKPNxXwDOzoFu3Gt8jFKwI0jsmkcII 4 | k1wC0CDQrsAm50uCla6zEGWLF/Fd5lfoXZ5Jo20LgHV7t9tXsZCzT3RgC0dKkrnG 5 | umJK5g1OYGN6l4whi2zOCMvIT/V5Am7epCZx1GS6O2mVLZhrfgpfFiRx6o1QV07X 6 | XnW+LFz8Rk5cwOUlt4K9h0DSc9p5yPvgz3DDw2qA4rcr7BK18ePUJR1khey8aDQW 7 | zHfSfVj8OwLlhLN8ixdXv+YLvGKaOsVH2cvt2wIDAQABAoIBAEvF8z3SfHJB5Arg 8 | kfBSYhrdv83mh7OAf0rTpFrkOPxjsYoIBggeUyEH8FRvSk9dqXCjcMHanpYG81yT 9 | cusbrxfQh7PCNPVPkIPJQ5BACapPfmLhoGGZc3pMknYxS5pCPuSmkOBtYr3ncTjY 10 | SX4XAJ+vs9fZmdzmZU0LX8rQ2ovGeFrbX0jRr3g6B85VsAeGJ9TDSSerPnO0mtFI 11 | ZSdl1wttuZErRRygt0zf00wlVh1iPXXe8J3oW4spFPwCPKahhNc7Ezz6HWo6ml+h 12 | Dlmbcjz1QIqKLd8DCmGh/gx7X1lC/AKEq2c6KIMDoaBKjICwh9Kho0/OsJQ3+BKn 13 | /yvBsvECgYEA+ccNBnmLd+VrLtTh7+dKRAlQsDkirnvZ3bpA3QJWAA97QYpz5riy 14 | IDQO3UV6jkcMbcaj3wOwKAi9k3FwCPaz93+//W6JpQWnwjH9c/ziogAJ/g6Pman/ 15 | hHTKJO044nCZh3WLQpSOwTz0yFRQM6q1cEollhT2hKnFxbtDzVZx+CMCgYEAwlu+ 16 | e6UAs3xrftiX70UkPbGNBLoP0wNJFqT4TautmVbUF3s2tSdyf/wbWuhTIsYYwRdv 17 | B5i7Or1e/DsVH82cq8z9ZB15EVTR4awshLVJz15RvqEtITfZrZ2hdFf0CKk/9J4w 18 | hzmRG14qz/GlL8CVqork5F4bd10jZscVdQqA8ukCgYBlCzEpvWG+TwDdISGFe3t/ 19 | qoUJxRNSoqewGvjCb3965shl6yyX2X+1p1mcCc9aX0OX5RPF1CgfCeonC2zXM3X6 20 | WaPBUkY8i90hojd2BIdqIbnpHNravvqvCs/7wDuS3xo8wkBj3tUhNxePMwx+2kAr 21 | /NLXtANGB6gKJYd4OdBBIQKBgD4gf3ocm2XETsRETgTQ8C28VJx/MVG9Sh6v6yNA 22 | zoQmijNbUniDvIkGuGPNwc1qzzzh1b7y5l53bCZqaG07F2qfYxweg7WzjEd79tsQ 23 | 7CAaQT0TXk6xAKcLrTF4b+xY1bXG3zJKh4TdDAhecPQbtnvGXDZXkqYMIqXW25gH 24 | HIMJAoGAUzABCDKoI1qsznMVqM7jC77akGHlwlWI9BmYhWOXIrO5th5CHFJK+YBE 25 | sG44dwnd2Sv/o2REn34cM8HETq7cyuW+Et2N+VyogyGlS3W+HCHkMYINU/ZPeGMF 26 | wiwXklON2CtatN31JDEeMusyySki5I/lw9at2TvDsdrLGeP4i3I= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test-fixtures/return_ok.wat: -------------------------------------------------------------------------------- 1 | (module 2 | (import "fastly_http_resp" "new" 3 | (func $response_new 4 | (param i32) 5 | (result i32))) 6 | (import "fastly_http_resp" "status_set" 7 | (func $response_set_status 8 | (param i32) (param i32) 9 | (result i32))) 10 | (import "fastly_http_resp" "send_downstream" 11 | (func $response_send 12 | (param i32) (param i32) (param i32) 13 | (result i32))) 14 | 15 | (import "wasi_snapshot_preview1" "proc_exit" 16 | (func $wasi_exit 17 | (param i32))) 18 | 19 | ;; we're going to fix a few memory locations as constants, just to avoid 20 | ;; some other messiness, even though it's bad software engineering. 21 | (global $response_handle_buffer i32 (i32.const 4)) 22 | 23 | (func $main (export "_start") 24 | (i32.const 200) 25 | (call $send_response) 26 | (i32.const 0) 27 | (call $wasi_exit) 28 | unreachable 29 | ) 30 | 31 | ;; Send a resposne back to the test harness, using the status code 32 | ;; provided in the first argument. This message will have no body, 33 | ;; and no headers, just the response code. It will fail catastrophically 34 | ;; (i.e., end this execution) if anything goes wrong in the process. 35 | (func $send_response (param $result i32) 36 | ;; create the response 37 | (global.get $response_handle_buffer) 38 | (call $response_new) 39 | (call $maybe_error_die) 40 | 41 | ;; set the status 42 | (global.get $response_handle_buffer) 43 | (i32.load) 44 | (local.get $result) 45 | (call $response_set_status) 46 | (call $maybe_error_die) 47 | 48 | ;; send it to the client 49 | (global.get $response_handle_buffer) 50 | (i32.load) 51 | (i32.const 0) ;; empty body 52 | (i32.const 0) ;; not streaming 53 | (call $response_send) 54 | (call $maybe_error_die) 55 | ) 56 | 57 | ;; this is not super informative, but: check to see if the status code 58 | ;; from whatever hostcall we just invoked came back as OK (0); if not, 59 | ;; immediately exit with the return value as a diagnostic. (Not that it 60 | ;; tells us where that value came from, but at least it's something?) 61 | (func $maybe_error_die (param $status_code i32) 62 | (block $test_block 63 | (local.get $status_code) 64 | (i32.const 0) 65 | (i32.ne) 66 | (br_if $test_block) 67 | (return) 68 | ) 69 | (local.get $status_code) 70 | (call $wasi_exit) 71 | unreachable 72 | ) 73 | 74 | (memory (;0;) 1) ;; 1 * 64k = 64k :) 75 | (export "memory" (memory 0)) 76 | 77 | ) -------------------------------------------------------------------------------- /test-fixtures/src/bin/acl.rs: -------------------------------------------------------------------------------- 1 | //! A guest program to test that acls works properly. 2 | use fastly::acl::Acl; 3 | use fastly::Error; 4 | use std::net::{Ipv4Addr, Ipv6Addr}; 5 | 6 | fn main() -> Result<(), Error> { 7 | match Acl::open("DOES-NOT-EXIST") { 8 | Err(fastly::acl::OpenError::AclNotFound) => { /* OK */ } 9 | Err(other) => panic!("expected error opening non-existant acl, got: {:?}", other), 10 | _ => panic!("expected error opening non-existant acl, got Ok"), 11 | } 12 | 13 | let acl1 = Acl::open("my-acl-1")?; 14 | 15 | match acl1.try_lookup(Ipv4Addr::new(192, 168, 1, 1).into())? { 16 | Some(lookup_match) => { 17 | assert_eq!(lookup_match.prefix(), "192.168.0.0/16"); 18 | assert!(lookup_match.is_block()); 19 | } 20 | None => panic!("expected match"), 21 | }; 22 | match acl1.try_lookup(Ipv4Addr::new(23, 23, 23, 23).into())? { 23 | Some(lookup_match) => { 24 | assert_eq!(lookup_match.prefix(), "23.23.23.23/32"); 25 | assert!(lookup_match.is_allow()); 26 | } 27 | None => panic!("expected match"), 28 | }; 29 | if let Some(lookup_match) = acl1.try_lookup(Ipv4Addr::new(100, 100, 100, 100).into())? { 30 | panic!("expected no match, got: {:?}", lookup_match); 31 | } 32 | 33 | let acl2 = Acl::open("my-acl-2")?; 34 | 35 | match acl2.try_lookup(Ipv6Addr::new(0x2000, 0, 0, 0, 0, 1, 2, 3).into())? { 36 | Some(lookup_match) => { 37 | assert_eq!(lookup_match.prefix(), "2000::/24"); 38 | assert!(lookup_match.is_block()); 39 | } 40 | None => panic!("expected match"), 41 | }; 42 | match acl2.try_lookup(Ipv6Addr::new(0xFACE, 0, 2, 3, 4, 5, 6, 7).into())? { 43 | Some(lookup_match) => { 44 | assert_eq!(lookup_match.prefix(), "face::/16"); 45 | assert!(lookup_match.is_allow()); 46 | } 47 | None => panic!("expected match"), 48 | }; 49 | if let Some(lookup_match) = 50 | acl2.try_lookup(Ipv6Addr::new(0xFADE, 1, 2, 3, 4, 5, 6, 7).into())? 51 | { 52 | panic!("expected no match, got: {:?}", lookup_match); 53 | }; 54 | 55 | Ok(()) 56 | } 57 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/args.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let args = Vec::from_iter(std::env::args()); 3 | 4 | assert_eq!(args.len(), 1); 5 | assert_eq!(args[0], "compute-app"); 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/bad-framing-headers.rs: -------------------------------------------------------------------------------- 1 | use fastly::http::{self, HeaderValue}; 2 | use fastly::{Error, Request}; 3 | 4 | fn main() -> Result<(), Error> { 5 | let mut req = Request::from_client(); 6 | 7 | // Send a backend request with a synthetic body and some bad headers 8 | Request::post("http://example.org/TheURL") 9 | // Even though we set a chunked transfer encoding, the backend should see a 10 | // content-length. This is hardcoded XQD behavior to strip this header. 11 | .with_header( 12 | http::header::TRANSFER_ENCODING, 13 | HeaderValue::from_static("chunked"), 14 | ) 15 | .with_body("synthetic") 16 | .with_pass(true) 17 | .send("TheOrigin")? 18 | .into_body_bytes(); 19 | 20 | // Now test forwarding the non-synthetic request from the client. 21 | 22 | // Set a bogus content-length and forward to the backend. 23 | req.set_header( 24 | http::header::CONTENT_LENGTH, 25 | HeaderValue::from_static("99999"), 26 | ); 27 | let mut resp = req.with_pass(true).send("TheOrigin")?; 28 | 29 | // Now set a bogus transfer encoding even though this should not be a chunked response 30 | resp.set_header( 31 | http::header::TRANSFER_ENCODING, 32 | HeaderValue::from_static("chunked"), 33 | ); 34 | resp.send_to_client(); 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/config-store-lookup.rs: -------------------------------------------------------------------------------- 1 | //! A guest program to test that config-store lookups work properly. 2 | 3 | use fastly_shared::FastlyStatus; 4 | 5 | fn main() { 6 | let animals = unsafe { 7 | let mut dict_handle = fastly_shared::INVALID_CONFIG_STORE_HANDLE; 8 | let res = fastly_sys::fastly_config_store::open( 9 | "animals".as_ptr(), 10 | "animals".len(), 11 | &mut dict_handle as *mut _, 12 | ); 13 | assert_eq!(res, FastlyStatus::OK, "Failed to open config-store"); 14 | dict_handle 15 | }; 16 | 17 | #[derive(Debug, PartialEq, Eq)] 18 | enum LookupResult { 19 | Success(String), 20 | Missing, 21 | BufTooSmall(usize), 22 | Err(FastlyStatus), 23 | } 24 | 25 | let get = |key: &str, buf_len: usize| unsafe { 26 | let mut value = Vec::with_capacity(buf_len); 27 | let mut nwritten = 0; 28 | let res = fastly_sys::fastly_config_store::get( 29 | animals, 30 | key.as_ptr(), 31 | key.len(), 32 | value.as_mut_ptr(), 33 | buf_len, 34 | &mut nwritten as *mut _, 35 | ); 36 | 37 | if res != FastlyStatus::OK { 38 | if res == FastlyStatus::NONE { 39 | return LookupResult::Missing; 40 | } 41 | 42 | if res == FastlyStatus::BUFLEN { 43 | assert!(nwritten > 0 && buf_len < nwritten); 44 | return LookupResult::BufTooSmall(nwritten); 45 | } 46 | 47 | return LookupResult::Err(res); 48 | } 49 | 50 | value.set_len(nwritten); 51 | value.shrink_to(nwritten); 52 | LookupResult::Success(String::from_utf8(value).unwrap()) 53 | }; 54 | 55 | assert_eq!(get("dog", 4), LookupResult::Success(String::from("woof"))); 56 | assert_eq!(get("cat", 4), LookupResult::Success(String::from("meow"))); 57 | assert_eq!(get("cat", 2), LookupResult::BufTooSmall(4)); 58 | assert_eq!(get("lamp", 4), LookupResult::Missing); 59 | } 60 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/content-length.rs: -------------------------------------------------------------------------------- 1 | //! Exercise calculation of Content-Length. 2 | use fastly::handle::{BodyHandle, RequestHandle, ResponseHandle}; 3 | use fastly::http::{header, Url}; 4 | use fastly::log::set_panic_endpoint; 5 | use fastly::Error; 6 | use std::io::{Read, Write}; 7 | 8 | fn main() -> Result<(), Error> { 9 | set_panic_endpoint("PANIC!")?; 10 | let mut r = RequestHandle::new(); 11 | r.set_url(&Url::parse("http://origin.org/")?); 12 | let b = BodyHandle::new(); 13 | let (resp, mut body) = r.send(b, "TheOrigin")?; 14 | 15 | assert_eq!(resp.get_status(), 200); 16 | 17 | assert_eq!( 18 | resp.get_header_value(&header::CONTENT_LENGTH, 10)? 19 | .expect("response should have Content-Length"), 20 | "20" 21 | ); 22 | 23 | let mut buf = [0; 4]; 24 | 25 | body.read_exact(&mut buf).expect("read should have succeed"); 26 | 27 | // Copy some bytes from the upstream response 28 | let mut newb = BodyHandle::from(&buf[..]); // 4 29 | 30 | // Add some synthetic bytes 31 | newb.write_all(b"12345")?; // ; 4 + 5 32 | 33 | // Append a synthetic body 34 | newb.append(BodyHandle::from("xyz")); // 4 + 5 + 3 35 | 36 | // Append the rest of the upstream body 37 | newb.append(body); // 4 + 5 + 3 + (20-4) = 28 38 | 39 | ResponseHandle::new().send_to_client(newb); 40 | 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/device-detection-lookup.rs: -------------------------------------------------------------------------------- 1 | // //! A guest program to test that Device Detection lookups work properly. 2 | 3 | use fastly::device_detection::lookup; 4 | 5 | fn main() { 6 | let ua = "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0 [FBAN/FBIOS;FBAV/8.0.0.28.18;FBBV/1665515;FBDV/iPhone4,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/7.0.4;FBSS/2; FBCR/Telekom.de;FBID/phone;FBLC/de_DE;FBOP/5]"; 7 | let device = lookup(&ua).unwrap(); 8 | assert_eq!(device.device_name(), Some("iPhone")); 9 | assert_eq!(device.brand(), Some("Apple")); 10 | assert_eq!(device.model(), Some("iPhone4,1")); 11 | assert_eq!(device.hwtype(), Some("Mobile Phone")); 12 | assert_eq!(device.is_ereader(), Some(false)); 13 | assert_eq!(device.is_gameconsole(), Some(false)); 14 | assert_eq!(device.is_mediaplayer(), Some(false)); 15 | assert_eq!(device.is_mobile(), Some(true)); 16 | assert_eq!(device.is_smarttv(), Some(false)); 17 | assert_eq!(device.is_tablet(), Some(false)); 18 | assert_eq!(device.is_tvplayer(), Some(false)); 19 | assert_eq!(device.is_desktop(), Some(false)); 20 | assert_eq!(device.is_touchscreen(), Some(true)); 21 | 22 | let ua = "ghosts-app/1.0.2.1 (ASUSTeK COMPUTER INC.; X550CC; Windows 8 (X86); en)"; 23 | let device = lookup(&ua).unwrap(); 24 | assert_eq!(device.device_name(), Some("Asus TeK")); 25 | assert_eq!(device.brand(), Some("Asus")); 26 | assert_eq!(device.model(), Some("TeK")); 27 | assert_eq!(device.hwtype(), None); 28 | assert_eq!(device.is_ereader(), None); 29 | assert_eq!(device.is_gameconsole(), None); 30 | assert_eq!(device.is_mediaplayer(), None); 31 | assert_eq!(device.is_mobile(), None); 32 | assert_eq!(device.is_smarttv(), None); 33 | assert_eq!(device.is_tablet(), None); 34 | assert_eq!(device.is_tvplayer(), None); 35 | assert_eq!(device.is_desktop(), Some(false)); 36 | assert_eq!(device.is_touchscreen(), None); 37 | } 38 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/dictionary-lookup.rs: -------------------------------------------------------------------------------- 1 | //! A guest program to test that dictionary lookups work properly. 2 | 3 | use fastly_shared::FastlyStatus; 4 | 5 | fn main() { 6 | let animals = unsafe { 7 | let mut dict_handle = fastly_shared::INVALID_DICTIONARY_HANDLE; 8 | let res = fastly_sys::fastly_dictionary::open( 9 | "animals".as_ptr(), 10 | "animals".len(), 11 | &mut dict_handle as *mut _, 12 | ); 13 | assert_eq!(res, FastlyStatus::OK, "Failed to open dictionary"); 14 | dict_handle 15 | }; 16 | 17 | #[derive(Debug, PartialEq, Eq)] 18 | enum LookupResult { 19 | Success(String), 20 | Missing, 21 | BufTooSmall(usize), 22 | Err(FastlyStatus), 23 | } 24 | 25 | let get = |key: &str, buf_len: usize| unsafe { 26 | let mut value = Vec::with_capacity(buf_len); 27 | let mut nwritten = 0; 28 | let res = fastly_sys::fastly_dictionary::get( 29 | animals, 30 | key.as_ptr(), 31 | key.len(), 32 | value.as_mut_ptr(), 33 | buf_len, 34 | &mut nwritten as *mut _, 35 | ); 36 | 37 | if res != FastlyStatus::OK { 38 | if res == FastlyStatus::NONE { 39 | return LookupResult::Missing; 40 | } 41 | 42 | if res == FastlyStatus::BUFLEN { 43 | assert!(nwritten > 0 && buf_len < nwritten); 44 | return LookupResult::BufTooSmall(nwritten); 45 | } 46 | 47 | return LookupResult::Err(res); 48 | } 49 | 50 | value.set_len(nwritten); 51 | value.shrink_to(nwritten); 52 | LookupResult::Success(String::from_utf8(value).unwrap()) 53 | }; 54 | 55 | assert_eq!(get("dog", 4), LookupResult::Success(String::from("woof"))); 56 | assert_eq!(get("cat", 4), LookupResult::Success(String::from("meow"))); 57 | assert_eq!(get("cat", 2), LookupResult::BufTooSmall(4)); 58 | assert_eq!(get("lamp", 4), LookupResult::Missing); 59 | } 60 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/downstream-req.rs: -------------------------------------------------------------------------------- 1 | use fastly::Request; 2 | use std::net::IpAddr; 3 | 4 | fn main() { 5 | let mut client_req = Request::from_client(); 6 | 7 | // Check that the actual request headers came through 8 | assert_eq!(client_req.get_header_str("Accept"), Some("text/html")); 9 | assert_eq!(client_req.get_header_str("X-Custom-Test"), Some("abcdef")); 10 | 11 | // Mutate the client request 12 | client_req.set_header("X-Custom-2", "added"); 13 | 14 | // Ensure that the methods for getting the original header info do _not_ 15 | // include the mutation 16 | let names: Vec = client_req.get_original_header_names().unwrap().collect(); 17 | assert_eq!( 18 | names, 19 | vec![String::from("accept"), String::from("x-custom-test")] 20 | ); 21 | assert_eq!(client_req.get_original_header_count().unwrap(), 2); 22 | 23 | assert_eq!(client_req.take_body_str(), "Hello, world!"); 24 | 25 | let localhost: IpAddr = "127.0.0.1".parse().unwrap(); 26 | assert_eq!(client_req.get_client_ip_addr().unwrap(), localhost); 27 | 28 | let localhost: IpAddr = "127.0.0.1".parse().unwrap(); 29 | assert_eq!(client_req.get_server_ip_addr().unwrap(), localhost); 30 | 31 | assert_eq!(client_req.get_tls_cipher_openssl_name(), None); 32 | assert_eq!(client_req.get_tls_cipher_openssl_name_bytes(), None); 33 | assert_eq!(client_req.get_tls_client_hello(), None); 34 | assert_eq!(client_req.get_tls_protocol(), None); 35 | assert_eq!(client_req.get_tls_protocol_bytes(), None); 36 | // NOTE: This currently fails, waiting on a patch to land in the fastly crate 37 | // assert_eq!(client_req.get_tls_raw_client_certificate(), None); 38 | assert_eq!(client_req.get_tls_raw_client_certificate_bytes(), None); 39 | } 40 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/edge-rate-limiting.rs: -------------------------------------------------------------------------------- 1 | //! A guest program to test that edge-rate-limiting API is implemented. 2 | 3 | use std::time::Duration; 4 | 5 | use fastly::erl::{CounterDuration, Penaltybox, RateCounter, RateWindow, ERL}; 6 | 7 | fn main() { 8 | let entry = "entry"; 9 | 10 | let rc = RateCounter::open("rc"); 11 | let pb = Penaltybox::open("pb"); 12 | let erl = ERL::open(rc, pb); 13 | 14 | let not_blocked = erl 15 | .check_rate(entry, 1, RateWindow::TenSecs, 100, Duration::from_secs(300)) 16 | .unwrap(); 17 | assert_eq!(not_blocked, false); 18 | 19 | let rc2 = RateCounter::open("rc"); 20 | let rate_1 = rc2.lookup_rate(entry, RateWindow::OneSec).unwrap(); 21 | assert_eq!(rate_1, 0); 22 | 23 | let count10 = rc2.lookup_count(entry, CounterDuration::TenSec).unwrap(); 24 | assert_eq!(count10, 0); 25 | 26 | assert!(rc2.increment(entry, 600).is_ok()); 27 | 28 | let pb2 = Penaltybox::open("pb"); 29 | let not_in_pb = pb2.has(entry).unwrap(); 30 | assert_eq!(not_in_pb, false); 31 | 32 | assert!(pb2.add(entry, Duration::from_secs(300)).is_ok()); 33 | } 34 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/env-vars.rs: -------------------------------------------------------------------------------- 1 | //! A guest program to test that Viceroy sets environment variables 2 | 3 | use std::env; 4 | 5 | fn main() { 6 | let _generation: u64 = env::var("FASTLY_CACHE_GENERATION") 7 | .expect("cache generation available") 8 | .parse() 9 | .expect("parses as u64"); 10 | 11 | let cid = env::var("FASTLY_CUSTOMER_ID").expect("customer ID available"); 12 | assert!(!cid.is_empty()); 13 | 14 | let pop = env::var("FASTLY_POP").expect("POP available"); 15 | assert_eq!(pop.len(), 3); 16 | 17 | let region = env::var("FASTLY_REGION").expect("region available"); 18 | assert!(!region.is_empty()); 19 | 20 | let sid = env::var("FASTLY_SERVICE_ID").expect("service ID available"); 21 | assert!(!sid.is_empty()); 22 | 23 | let _version: u64 = env::var("FASTLY_SERVICE_VERSION") 24 | .expect("service version available") 25 | .parse() 26 | .expect("parses as u64"); 27 | 28 | let id = env::var("FASTLY_TRACE_ID").expect("trace ID available"); 29 | u64::from_str_radix(&id, 16).expect("parses as u64"); 30 | 31 | let host_name = env::var("FASTLY_HOSTNAME").expect("host name available"); 32 | assert_eq!(host_name, "localhost"); 33 | 34 | let is_staging = env::var("FASTLY_IS_STAGING").expect("staging variable set"); 35 | 36 | assert!(is_staging == "0" || is_staging == "1"); 37 | } 38 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/expects-hello.rs: -------------------------------------------------------------------------------- 1 | use fastly::Request; 2 | use http::{HeaderName, StatusCode, Version}; 3 | 4 | fn main() { 5 | let mut resp = Request::get("https://fastly.com/") 6 | // .with_version(Version::HTTP_2) 7 | .with_header(HeaderName::from_static("accept-encoding"), "gzip") 8 | .with_auto_decompress_gzip(true) 9 | .send("ReturnsHello") 10 | .expect("can send request"); 11 | assert_eq!(resp.get_status(), StatusCode::OK); 12 | let got = resp.take_body_str(); 13 | eprintln!("got: {}", got); 14 | 15 | assert_eq!(&got, "hello world", "got: {}", got); 16 | } 17 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/geolocation-lookup-default.rs: -------------------------------------------------------------------------------- 1 | //! A guest program to test that Geolocation lookups work properly. 2 | 3 | use fastly::geo::{ 4 | geo_lookup, 5 | ConnSpeed, 6 | ConnType, 7 | Continent, 8 | ProxyDescription, 9 | ProxyType, 10 | // UtcOffset, 11 | }; 12 | use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; 13 | 14 | fn main() { 15 | let client_ip_v4 = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); 16 | let geo_v4 = geo_lookup(client_ip_v4).unwrap(); 17 | 18 | assert_eq!(geo_v4.as_name(), "Fastly, Inc"); 19 | assert_eq!(geo_v4.as_number(), 54113); 20 | assert_eq!(geo_v4.area_code(), 415); 21 | assert_eq!(geo_v4.city(), "San Francisco"); 22 | assert_eq!(geo_v4.conn_speed(), ConnSpeed::Broadband); 23 | assert_eq!(geo_v4.conn_type(), ConnType::Wired); 24 | assert_eq!(geo_v4.continent(), Continent::NorthAmerica); 25 | assert_eq!(geo_v4.country_code(), "US"); 26 | assert_eq!(geo_v4.country_code3(), "USA"); 27 | assert_eq!(geo_v4.country_name(), "United States of America"); 28 | assert_eq!(geo_v4.latitude(), 37.77869); 29 | assert_eq!(geo_v4.longitude(), -122.39557); 30 | assert_eq!(geo_v4.metro_code(), 0); 31 | assert_eq!(geo_v4.postal_code(), "94107"); 32 | assert_eq!(geo_v4.proxy_description(), ProxyDescription::Unknown); 33 | assert_eq!(geo_v4.proxy_type(), ProxyType::Unknown); 34 | assert_eq!(geo_v4.region(), Some("CA")); 35 | // commented out because the below line fails both in Viceroy and Compute. 36 | // assert_eq!(geo_v4.utc_offset(), Some(UtcOffset::from_hms(-7, 0, 0).unwrap()); 37 | 38 | let client_ip_v6 = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)); 39 | let geo_v6 = geo_lookup(client_ip_v6).unwrap(); 40 | 41 | assert_eq!(geo_v6.as_name(), "Fastly, Inc"); 42 | assert_eq!(geo_v6.as_number(), 54113); 43 | assert_eq!(geo_v6.area_code(), 415); 44 | assert_eq!(geo_v6.city(), "San Francisco"); 45 | assert_eq!(geo_v6.conn_speed(), ConnSpeed::Broadband); 46 | assert_eq!(geo_v6.conn_type(), ConnType::Wired); 47 | assert_eq!(geo_v6.continent(), Continent::NorthAmerica); 48 | assert_eq!(geo_v6.country_code(), "US"); 49 | assert_eq!(geo_v6.country_code3(), "USA"); 50 | assert_eq!(geo_v6.country_name(), "United States of America"); 51 | assert_eq!(geo_v6.latitude(), 37.77869); 52 | assert_eq!(geo_v6.longitude(), -122.39557); 53 | assert_eq!(geo_v6.metro_code(), 0); 54 | assert_eq!(geo_v6.postal_code(), "94107"); 55 | assert_eq!(geo_v6.proxy_description(), ProxyDescription::Unknown); 56 | assert_eq!(geo_v6.proxy_type(), ProxyType::Unknown); 57 | assert_eq!(geo_v6.region(), Some("CA")); 58 | // commented out because the below line fails both in Viceroy and Compute. 59 | // assert_eq!(geo_v6.utc_offset(), Some(UtcOffset::from_hms(-7, 0, 0).unwrap()); 60 | } 61 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/geolocation-lookup.rs: -------------------------------------------------------------------------------- 1 | //! A guest program to test that Geolocation lookups work properly. 2 | 3 | use fastly::geo::{ 4 | geo_lookup, 5 | ConnSpeed, 6 | ConnType, 7 | Continent, 8 | ProxyDescription, 9 | ProxyType, 10 | // UtcOffset, 11 | }; 12 | use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; 13 | 14 | fn main() { 15 | let client_ip_v4 = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); 16 | let geo_v4 = geo_lookup(client_ip_v4).unwrap(); 17 | assert_eq!(geo_v4.as_name(), "Fastly Test"); 18 | assert_eq!(geo_v4.as_number(), 12345); 19 | assert_eq!(geo_v4.area_code(), 123); 20 | assert_eq!(geo_v4.city(), "Test City"); 21 | assert_eq!(geo_v4.conn_speed(), ConnSpeed::Broadband); 22 | assert_eq!(geo_v4.conn_type(), ConnType::Wired); 23 | assert_eq!(geo_v4.continent(), Continent::NorthAmerica); 24 | assert_eq!(geo_v4.country_code(), "CA"); 25 | assert_eq!(geo_v4.country_code3(), "CAN"); 26 | assert_eq!(geo_v4.country_name(), "Canada"); 27 | assert_eq!(geo_v4.latitude(), 12.345); 28 | assert_eq!(geo_v4.longitude(), 54.321); 29 | assert_eq!(geo_v4.metro_code(), 0); 30 | assert_eq!(geo_v4.postal_code(), "12345"); 31 | assert_eq!(geo_v4.proxy_description(), ProxyDescription::Unknown); 32 | assert_eq!(geo_v4.proxy_type(), ProxyType::Unknown); 33 | assert_eq!(geo_v4.region(), Some("BC")); 34 | // commented out because the below line fails both in Viceroy and Compute. 35 | // assert_eq!(geo_v4.utc_offset(), Some(UtcOffset::from_hms(-7, 0, 0).unwrap())); 36 | 37 | let client_ip_v6 = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)); 38 | let geo_v6 = geo_lookup(client_ip_v6).unwrap(); 39 | assert_eq!(geo_v6.as_name(), "Fastly Test IPv6"); 40 | assert_eq!(geo_v6.city(), "Test City IPv6"); 41 | } 42 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/grpc.rs: -------------------------------------------------------------------------------- 1 | use fastly::experimental::GrpcBackend; 2 | use fastly::{Backend, Error, Request}; 3 | use std::str::FromStr; 4 | 5 | /// Pass everything from the downstream request through to the backend, then pass everything back 6 | /// from the upstream request to the downstream response. 7 | fn main() -> Result<(), Error> { 8 | let client_req = Request::from_client(); 9 | let Some(port_str) = client_req.get_header_str("Port") else { 10 | panic!("Couldn't find out what port to use!"); 11 | }; 12 | let port = u16::from_str(port_str).unwrap(); 13 | 14 | let backend = Backend::builder("grpc-backend", format!("localhost:{}", port)) 15 | .for_grpc(true) 16 | .finish() 17 | .expect("can build backend"); 18 | 19 | Request::get("http://localhost/") 20 | .with_header("header", "is-a-thing") 21 | .with_body("hello") 22 | .send(backend) 23 | .unwrap() 24 | .send_to_client(); 25 | 26 | Ok(()) 27 | } 28 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/inspect.rs: -------------------------------------------------------------------------------- 1 | use fastly::handle::BodyHandle; 2 | use fastly::http::StatusCode; 3 | use fastly::security::{inspect, InspectConfig}; 4 | use fastly::{Error, Request, Response}; 5 | 6 | #[fastly::main] 7 | fn main(req: Request) -> Result { 8 | let (req, body) = req.into_handles(); 9 | let body = body.unwrap_or_else(BodyHandle::new); 10 | 11 | let inspectconf = InspectConfig::new(&req, &body) 12 | .corp("junichi-lab") 13 | .workspace("lab"); 14 | 15 | let resp = match inspect(inspectconf) { 16 | Ok(x) => { 17 | let body = format!( 18 | "inspect result: waf_response={}, tags={:?}, decision_ms={}ms, verdict={:?}", 19 | x.status(), 20 | x.tags(), 21 | x.decision_ms().as_millis(), 22 | x.verdict() 23 | ); 24 | 25 | Response::from_status(StatusCode::OK).with_body_text_plain(&body) 26 | } 27 | Err(e) => { 28 | let body = format!("Error: {e:?}"); 29 | 30 | Response::from_status(StatusCode::BAD_REQUEST).with_body_text_plain(&body) 31 | } 32 | }; 33 | 34 | Ok(resp) 35 | } 36 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/kv_store.rs: -------------------------------------------------------------------------------- 1 | //! A guest program to test that KV store works properly. 2 | 3 | use fastly::kv_store::{ 4 | KVStore, 5 | KVStoreError::{ItemNotFound, StoreNotFound}, 6 | }; 7 | 8 | fn main() { 9 | // Check we can't get a store that does not exist 10 | match KVStore::open("non_existant") { 11 | Err(StoreNotFound(_)) => {} 12 | _ => panic!(), 13 | } 14 | 15 | let store_one = KVStore::open("store_one").unwrap().unwrap(); 16 | // Test that we can get data using the `data` config key 17 | assert_eq!( 18 | store_one.lookup("first").unwrap().take_body().into_string(), 19 | "This is some data" 20 | ); 21 | // Test that we can get data from a file using the `path` config key 22 | assert_eq!( 23 | store_one 24 | .lookup("second") 25 | .unwrap() 26 | .take_body() 27 | .into_string(), 28 | "More data" 29 | ); 30 | // Test that we can get metadata using the `metadata` config key 31 | assert_eq!( 32 | store_one.lookup("third").unwrap().metadata().unwrap(), 33 | "some metadata" 34 | ); 35 | // Test that we cannot get metadata if it's not set 36 | assert_eq!( 37 | store_one.lookup("first").unwrap().metadata(), 38 | None 39 | ); 40 | 41 | let empty_store = KVStore::open("empty_store").unwrap().unwrap(); 42 | // Check that the value "bar" is not in the store 43 | match empty_store.lookup("bar") { 44 | Err(ItemNotFound) => {} 45 | _ => panic!(), 46 | } 47 | empty_store.insert("bar", "foo").unwrap(); 48 | // Check that the value "bar" is now in the store 49 | assert_eq!( 50 | empty_store.lookup("bar").unwrap().take_body().into_string(), 51 | "foo" 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/logging.rs: -------------------------------------------------------------------------------- 1 | use fastly::log::Endpoint; 2 | use std::io::Write; 3 | 4 | fn main() { 5 | let mut mib = Endpoint::from_name("mib"); 6 | let mut inigo = Endpoint::from_name("inigo"); 7 | 8 | inigo.write_all(b"Who are you?").unwrap(); 9 | mib.write_all(b"No one of consequence.").unwrap(); 10 | inigo.write_all(b"I must know.").unwrap(); 11 | mib.write_all(b"Get used to disappointment.").unwrap(); 12 | 13 | mib.write_all(b"There is something\nI ought to tell you.") 14 | .unwrap(); 15 | inigo.write_all(b"Tell me.\n\n").unwrap(); 16 | mib.write_all(b"I'm not left-handed either.").unwrap(); 17 | inigo.write_all(b"\n").unwrap(); // this event should be dropped 18 | inigo.write_all(b"O_O\n").unwrap(); 19 | 20 | println!("logging from stdout"); 21 | eprintln!("logging from stderr"); 22 | println!(""); // this should be dropped 23 | eprintln!(""); // this should be dropped 24 | 25 | // showcase line buffering on stdout 26 | print!("log line terminates on flush"); 27 | std::io::stdout().flush().unwrap(); 28 | print!("newline completes"); 29 | println!(" a log line"); 30 | 31 | // showcase no buffering on stderr 32 | eprint!("log line terminates on flush"); 33 | std::io::stderr().flush().unwrap(); 34 | eprint!("log line terminates"); 35 | eprint!("on each write"); 36 | 37 | assert!(Endpoint::try_from_name("stdout").is_err()); 38 | assert!(Endpoint::try_from_name("stderr").is_err()); 39 | assert!(Endpoint::try_from_name("STDOUT").is_err()); 40 | } 41 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/mutual-tls.rs: -------------------------------------------------------------------------------- 1 | use base64::engine::{general_purpose, Engine}; 2 | use fastly::http::StatusCode; 3 | use fastly::secret_store::Secret; 4 | use fastly::{Backend, Error, Request, Response}; 5 | use std::str::FromStr; 6 | 7 | /// Pass everything from the downstream request through to the backend, then pass everything back 8 | /// from the upstream request to the downstream response. 9 | fn main() -> Result<(), Error> { 10 | let client_req = Request::from_client(); 11 | let certificate = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/data/client.crt")); 12 | let key_bytes = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/data/client.key")); 13 | let key_secret = Secret::from_bytes(key_bytes.to_vec()).expect("can inject key"); 14 | 15 | let Some(port_str) = client_req.get_header_str("Port") else { 16 | panic!("Couldn't find out what port to use!"); 17 | }; 18 | let port = u16::from_str(port_str).unwrap(); 19 | 20 | let backend = Backend::builder("mtls-backend", format!("localhost:{}", port)) 21 | .enable_ssl() 22 | .provide_client_certificate(certificate, key_secret); 23 | 24 | let backend = if client_req.contains_header("set-ca") { 25 | backend.ca_certificate(include_str!(concat!( 26 | env!("CARGO_MANIFEST_DIR"), 27 | "/data/ca.pem" 28 | ))) 29 | } else { 30 | backend 31 | }; 32 | 33 | let backend = backend.finish().expect("can build backend"); 34 | 35 | let resp = Request::get("http://localhost/") 36 | .with_header("header", "is-a-thing") 37 | .with_body("hello") 38 | .send(backend); 39 | 40 | match resp { 41 | Ok(resp) => { 42 | assert_eq!(resp.get_status(), StatusCode::OK); 43 | let body = resp.into_body().into_string(); 44 | let mut cert_cursor = std::io::Cursor::new(certificate); 45 | let mut info = rustls_pemfile::certs(&mut cert_cursor).expect("got certs"); 46 | assert_eq!(info.len(), 1); 47 | let reflected_cert = info.remove(0); 48 | let base64_cert = general_purpose::STANDARD.encode(reflected_cert); 49 | assert_eq!(body, base64_cert); 50 | 51 | Response::from_status(200) 52 | .with_body("Hello, Viceroy!") 53 | .send_to_client(); 54 | } 55 | Err(e) => { 56 | Response::from_status(StatusCode::SERVICE_UNAVAILABLE) 57 | .with_body(format!("much sadness: {}", e)) 58 | .send_to_client(); 59 | } 60 | } 61 | 62 | Ok(()) 63 | } 64 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/noop.rs: -------------------------------------------------------------------------------- 1 | //! An empty guest program. 2 | 3 | fn main() {} 4 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/panic.rs: -------------------------------------------------------------------------------- 1 | // A simple guest that panics. 2 | fn main() { 3 | panic!(); 4 | } 5 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/secret-store.rs: -------------------------------------------------------------------------------- 1 | //! A guest program to test that secret store works properly. 2 | 3 | use fastly::secret_store::Secret; 4 | use fastly::SecretStore; 5 | 6 | fn main() { 7 | // Check we can't get a store that does not exist 8 | match SecretStore::open("nonexistent") { 9 | Err(_) => {} 10 | _ => panic!(), 11 | } 12 | 13 | let store_one = SecretStore::open("store_one").unwrap(); 14 | assert_eq!( 15 | store_one.get("first").unwrap().plaintext(), 16 | "This is some data" 17 | ); 18 | assert_eq!(store_one.get("second").unwrap().plaintext(), "More data"); 19 | 20 | match store_one.try_get("third").unwrap() { 21 | None => {} 22 | _ => panic!(), 23 | } 24 | 25 | let store_two = SecretStore::open("store_two").unwrap(); 26 | assert_eq!(store_two.get("first").unwrap().plaintext(), "first secret"); 27 | assert_eq!( 28 | store_two.get("second").unwrap().plaintext(), 29 | "another secret", 30 | ); 31 | 32 | let hello_bytes = "hello, wasm_world!".as_bytes().to_vec(); 33 | let secret = Secret::from_bytes(hello_bytes).unwrap(); 34 | assert_eq!("hello, wasm_world!", secret.plaintext()); 35 | } 36 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/sleep.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | std::thread::sleep(std::time::Duration::from_millis(100)) 3 | } 4 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/streaming-response.rs: -------------------------------------------------------------------------------- 1 | use fastly::Response; 2 | use std::io::Write; 3 | 4 | fn main() { 5 | let mut stream = Response::new().stream_to_client(); 6 | 7 | for i in 0..1000 { 8 | writeln!(stream, "{}", i).unwrap(); 9 | } 10 | 11 | stream.finish().unwrap(); 12 | } 13 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/teapot-status.rs: -------------------------------------------------------------------------------- 1 | //! Send a [`418 I'm a teapot`][tea] response downstream. 2 | //! 3 | //! `teapot-status.wasm` will create a [`418 I'm a teapot`][tea] response, per [RFC2324][rfc]. This 4 | //! status code is used to clearly indicate that a response came from the guest program. 5 | //! 6 | //! [rfc]: https://tools.ietf.org/html/rfc2324 7 | //! [tea]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/418 8 | 9 | use fastly_sys::{ 10 | fastly_http_body as http_body, fastly_http_resp as http_resp, BodyHandle, ResponseHandle, 11 | }; 12 | 13 | fn main() { 14 | let mut body: BodyHandle = 0; 15 | let mut resp: ResponseHandle = 0; 16 | unsafe { 17 | // Create a new response, set its status code, and send it downstream. 18 | http_resp::new(&mut resp) 19 | .result() 20 | .expect("can create a new response"); 21 | http_resp::status_set(resp, 418) 22 | .result() 23 | .expect("can set the status code"); 24 | http_body::new(&mut body) 25 | .result() 26 | .expect("can create a new body"); 27 | http_resp::send_downstream(resp, body, 0) 28 | .result() 29 | .expect("can send the response downstream"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/unknown-import.rs: -------------------------------------------------------------------------------- 1 | use fastly::{Request, Response}; 2 | use std::os::raw::{c_float, c_int}; 3 | 4 | /// This test fixture forwards the client request to a backend after calling a custom imported 5 | /// function that is not defined by Viceroy. 6 | fn main() { 7 | let client_req = Request::from_client(); 8 | 9 | if client_req.contains_header("call-it") { 10 | let unknown_result = unsafe { unknown_function(42, 120.0) }; 11 | // With the default mode, we don't even end up running this program. In trapping mode, we don't 12 | // make it past the function call above. It's only in "default value" mode that we make it here, 13 | // where the answer should be zero. 14 | assert_eq!(unknown_result, 0); 15 | } 16 | 17 | // Forward the request to the given backend 18 | client_req 19 | .send("TheOrigin") 20 | .unwrap_or_else(|_| Response::from_status(500)) 21 | .send_to_client(); 22 | } 23 | 24 | #[link(wasm_import_module = "unknown_module")] 25 | extern "C" { 26 | #[link_name = "unknown_function"] 27 | pub fn unknown_function(arg1: c_int, arg2: c_float) -> c_int; 28 | } 29 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/upstream-dynamic.rs: -------------------------------------------------------------------------------- 1 | use fastly::experimental::BackendCreationError; 2 | use fastly::{Backend, Error, Request, Response}; 3 | 4 | /// Pass everything from the downstream request through to the backend, then pass everything back 5 | /// from the upstream request to the downstream response. 6 | fn main() -> Result<(), Error> { 7 | let client_req = Request::from_client(); 8 | 9 | let backend = if let Some(dynamic_string) = client_req.get_header_str("Dynamic-Backend") { 10 | let mut basic_builder = Backend::builder("dynamic-backend", dynamic_string); 11 | 12 | if let Some(override_host) = client_req.get_header_str("With-Override") { 13 | basic_builder = basic_builder.override_host(override_host); 14 | } 15 | 16 | match basic_builder.finish() { 17 | Ok(x) => x, 18 | Err(err) => { 19 | match err { 20 | BackendCreationError::Disallowed => Response::from_status(403).send_to_client(), 21 | BackendCreationError::NameInUse => Response::from_status(409).send_to_client(), 22 | _ => Response::from_status(500).send_to_client(), 23 | } 24 | 25 | return Ok(()); 26 | } 27 | } 28 | } else if let Some(static_string) = client_req.get_header_str("Static-Backend") { 29 | Backend::from_name(static_string)? 30 | } else { 31 | panic!("Couldn't find a backend to use!"); 32 | }; 33 | 34 | if let Some(supplementary_backend) = client_req.get_header_str("Supplementary-Backend") { 35 | match Backend::builder(supplementary_backend, "fastly.com").finish() { 36 | Ok(_) => {} 37 | Err(err) => { 38 | match err { 39 | BackendCreationError::Disallowed => Response::from_status(403).send_to_client(), 40 | BackendCreationError::NameInUse => Response::from_status(409).send_to_client(), 41 | _ => Response::from_status(500).send_to_client(), 42 | } 43 | 44 | return Ok(()); 45 | } 46 | } 47 | } 48 | 49 | client_req 50 | .send(backend) 51 | .unwrap_or_else(|_| Response::from_status(503)) 52 | .send_to_client(); 53 | 54 | Ok(()) 55 | } 56 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/upstream-streaming.rs: -------------------------------------------------------------------------------- 1 | use fastly::Request; 2 | use std::io::Write; 3 | 4 | fn main() { 5 | let (mut stream, req) = Request::post("http://www.example.com/") 6 | .send_async_streaming("origin") 7 | .unwrap(); 8 | 9 | for i in 0..1000 { 10 | writeln!(stream, "{}", i).unwrap(); 11 | } 12 | 13 | stream.finish().unwrap(); 14 | req.wait().unwrap().send_to_client(); 15 | } 16 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/upstream.rs: -------------------------------------------------------------------------------- 1 | use fastly::{Backend, Request, Response}; 2 | 3 | /// This test fixture simply forwards the client request to a desired backend, specified by 4 | /// a special `Viceroy-Backend` header. 5 | fn main() { 6 | let client_req = Request::from_client(); 7 | 8 | // Extract the desired backend from a the `Viceroy-Backend` header 9 | let backend_name = client_req 10 | .get_header_str("Viceroy-Backend") 11 | .expect("No backend header"); 12 | let backend = Backend::from_name(backend_name).expect("Could not parse backend name"); 13 | 14 | // Forward the request to the given backend 15 | client_req 16 | .send(backend) 17 | .unwrap_or_else(|_| Response::from_status(500)) 18 | .send_to_client(); 19 | } 20 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/vcpu_time_test.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use fastly::{Error, Request, Response}; 3 | use fastly_shared::FastlyStatus; 4 | use hex_literal::hex; 5 | use sha2::{Sha512, Digest}; 6 | use std::time::Instant; 7 | 8 | #[link(wasm_import_module = "fastly_compute_runtime")] 9 | extern "C" { 10 | #[link_name = "get_vcpu_ms"] 11 | pub fn get_vcpu_ms(ms_out: *mut u64) -> FastlyStatus; 12 | } 13 | 14 | fn current_vcpu_ms() -> Result { 15 | let mut vcpu_time = 0u64; 16 | let vcpu_time_result = unsafe { get_vcpu_ms(&mut vcpu_time) }; 17 | if vcpu_time_result != FastlyStatus::OK { 18 | return Err(anyhow!("Got bad response from get_vcpu_ms: {:?}", vcpu_time_result)); 19 | } 20 | Ok(vcpu_time) 21 | } 22 | 23 | fn test_that_waiting_for_servers_increases_only_wall_time(client_req: Request) -> Result<(), Error> { 24 | let wall_initial_time = Instant::now(); 25 | let vcpu_initial_time = current_vcpu_ms()?; 26 | let Ok(_) = client_req.send("slow-server") else { 27 | Response::from_status(500).send_to_client(); 28 | return Ok(()); 29 | }; 30 | let wall_elapsed_time = wall_initial_time.elapsed().as_millis(); 31 | let vcpu_final_time = current_vcpu_ms()?; 32 | 33 | assert!( (vcpu_final_time - vcpu_initial_time) < 1000 ); 34 | assert!(wall_elapsed_time > 3000 ); 35 | 36 | Ok(()) 37 | } 38 | 39 | fn test_that_computing_factorial_increases_vcpu_time() -> Result<(), Error> { 40 | let vcpu_initial_time = current_vcpu_ms()?; 41 | 42 | let block = vec![0; 4096]; 43 | let mut written = 0; 44 | let mut hasher = Sha512::new(); 45 | while written < (1024 * 1024 * 1024) { 46 | hasher.update(&block); 47 | written += block.len(); 48 | } 49 | let result = hasher.finalize(); 50 | assert_eq!(result[..], hex!(" 51 | c5041ae163cf0f65600acfe7f6a63f212101687 52 | d41a57a4e18ffd2a07a452cd8175b8f5a4868dd 53 | 2330bfe5ae123f18216bdbc9e0f80d131e64b94 54 | 913a7b40bb5 55 | ")[..]); 56 | 57 | let vcpu_final_time = current_vcpu_ms()?; 58 | assert!(vcpu_final_time - vcpu_initial_time > 10000); 59 | Ok(()) 60 | } 61 | 62 | fn main() -> Result<(), Error> { 63 | let client_req = Request::from_client(); 64 | 65 | test_that_waiting_for_servers_increases_only_wall_time(client_req)?; 66 | test_that_computing_factorial_increases_vcpu_time()?; 67 | 68 | Response::from_status(200).send_to_client(); 69 | Ok(()) 70 | } 71 | -------------------------------------------------------------------------------- /test-fixtures/src/bin/write-and-read-body.rs: -------------------------------------------------------------------------------- 1 | use fastly::Body; 2 | use std::io::Read; 3 | 4 | fn main() { 5 | let mut body = Body::new(); 6 | body.write_str("Hello"); 7 | body.write_str(", "); 8 | 9 | let mut body2 = Body::new(); 10 | body2.write_str("Viceroy"); 11 | body2.write_str("!"); 12 | 13 | body.append(body2); 14 | 15 | let mut buf: Vec = vec![0, 0]; 16 | let res = body.read(buf.as_mut_slice()); 17 | assert_eq!(res.unwrap(), 2); 18 | assert_eq!(buf, &b"He"[..]); 19 | 20 | assert_eq!(body.into_string(), "llo, Viceroy!"); 21 | } 22 | -------------------------------------------------------------------------------- /test-fixtures/src/limits.rs: -------------------------------------------------------------------------------- 1 | // MAX_HEADER_NAME_LEN is defined in lib/src/wiggle_abi/headers.rs but cannot 2 | // be imported due to compilation of this workspace to the wasi32-wasm target. 3 | // In a more perfect world, the Hyper crate would export a constant for this 4 | // value since it panics when attempting to parse header names longer than 5 | // 32,768. 6 | pub const MAX_HEADER_NAME_LEN: usize = (1 << 16) - 1; 7 | --------------------------------------------------------------------------------