├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── client │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs └── server │ ├── .cargo │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ └── src │ └── lib.rs ├── scripts └── vendor-wit.sh ├── test-programs ├── Cargo.toml ├── artifacts │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ └── lib.rs └── src │ └── bin │ ├── client_get_chunk.rs │ ├── client_get_headers.rs │ ├── client_get_with_query.rs │ ├── client_post_with_body.rs │ ├── client_post_with_form_data.rs │ ├── client_post_with_json_data.rs │ ├── client_post_with_multipart_form_data.rs │ ├── server_authority.rs │ ├── server_form.rs │ ├── server_json.rs │ ├── server_large_body.rs │ ├── server_multipart_form.rs │ ├── server_query.rs │ └── server_status_code.rs ├── waki-macros ├── Cargo.toml └── src │ ├── dummy.rs │ ├── export.rs │ └── lib.rs └── waki ├── Cargo.toml ├── src ├── body.rs ├── client.rs ├── common │ ├── header.rs │ ├── mod.rs │ ├── request_and_response.rs │ └── scheme.rs ├── lib.rs ├── multipart │ ├── constants.rs │ ├── mod.rs │ └── parser.rs ├── request.rs └── response.rs ├── tests └── all │ ├── client.rs │ ├── fixtures │ └── file.txt │ ├── main.rs │ └── server.rs └── wit ├── deps ├── cli │ ├── command.wit │ ├── environment.wit │ ├── exit.wit │ ├── imports.wit │ ├── run.wit │ ├── stdio.wit │ └── terminal.wit ├── clocks │ ├── monotonic-clock.wit │ ├── timezone.wit │ ├── wall-clock.wit │ └── world.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 └── world.wit /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - 'main' 6 | - 'release-**' 7 | pull_request: 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 30 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Install Rust 15 | uses: dtolnay/rust-toolchain@1.82.0 16 | with: 17 | targets: wasm32-wasip2 18 | components: clippy, rustfmt 19 | - name: Re-vendor WIT 20 | run: | 21 | ./scripts/vendor-wit.sh 22 | git diff --exit-code 23 | - name: cargo fmt 24 | run: cargo fmt --all -- --check 25 | - name: cargo clippy 26 | run: cargo clippy --all-targets --all-features -- -D warnings 27 | test: 28 | needs: lint 29 | runs-on: ubuntu-latest 30 | timeout-minutes: 30 31 | strategy: 32 | matrix: 33 | include: 34 | - rust: "1.81" 35 | targets: "wasm32-wasip1" 36 | - rust: "1.82" 37 | targets: "wasm32-wasip2" 38 | name: Test on target ${{ matrix.targets }} 39 | steps: 40 | - uses: actions/checkout@v4 41 | - name: Install Rust 42 | uses: dtolnay/rust-toolchain@master 43 | with: 44 | toolchain: ${{ matrix.rust }} 45 | targets: ${{ matrix.targets }} 46 | - name: cargo test 47 | run: cargo test 48 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | permissions: 7 | contents: write 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 1 15 | - name: Install Rust 16 | uses: dtolnay/rust-toolchain@1.82.0 17 | - name: cargo publish 18 | run: | 19 | cargo publish -p waki-macros 20 | cargo publish -p waki 21 | env: 22 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | .idea 17 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "waki", 5 | "waki-macros", 6 | "test-programs", 7 | ] 8 | exclude = ["examples"] 9 | 10 | [workspace.package] 11 | version = "0.5.1" 12 | authors = ["Xinzhao Xu"] 13 | edition = "2021" 14 | categories = ["wasm"] 15 | keywords = ["webassembly", "wasm", "wasi"] 16 | repository = "https://github.com/wacker-dev/waki" 17 | license = "Apache-2.0" 18 | description = "HTTP client and server library for WASI" 19 | readme = "README.md" 20 | 21 | [workspace.dependencies] 22 | waki-macros = { path = "waki-macros", version = "0.5.1" } 23 | 24 | anyhow = "1.0.89" 25 | serde = "1.0.210" 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # waki 2 | 3 | HTTP client and server library for WASI. 4 | 5 | Send a request: 6 | 7 | ```rust 8 | let resp = Client::new() 9 | .post("https://httpbin.org/post") 10 | .connect_timeout(Duration::from_secs(5)) 11 | .send()?; 12 | 13 | println!("status code: {}", resp.status_code()); 14 | ``` 15 | 16 | Writing an HTTP component: 17 | 18 | ```rust 19 | use waki::{handler, ErrorCode, Request, Response}; 20 | 21 | #[handler] 22 | fn hello(req: Request) -> Result { 23 | Response::builder().body(b"Hello, WASI!").build() 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /examples/client/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-wasip2" 3 | -------------------------------------------------------------------------------- /examples/client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "http-client" 3 | version = "0.0.0" 4 | edition = "2021" 5 | rust-version = "1.82" 6 | publish = false 7 | 8 | [dependencies] 9 | waki = { version = "0.4.2", features = ["json", "multipart"] } 10 | serde = { version = "1.0.202", features = ["derive"] } 11 | 12 | # reduce wasm binary size 13 | [profile.release] 14 | lto = true 15 | strip = "symbols" 16 | -------------------------------------------------------------------------------- /examples/client/README.md: -------------------------------------------------------------------------------- 1 | # http-client 2 | 3 | This example is built using the [WASI Preview 2 API](https://github.com/WebAssembly/wasi-http) 4 | with the [waki](https://github.com/wacker-dev/waki) library. 5 | 6 | ## Build 7 | 8 | Requires Rust 1.82+. 9 | 10 | ``` 11 | $ rustup target add wasm32-wasip2 12 | ``` 13 | 14 | Use the following command to compile it into a WASM program: 15 | 16 | ``` 17 | $ cargo build 18 | ``` 19 | 20 | Or use `--release` option to build it in release mode: 21 | 22 | ``` 23 | $ cargo build --release 24 | ``` 25 | 26 | ## Run 27 | 28 | After compilation, you can use [wasmtime](https://github.com/bytecodealliance/wasmtime) to run it: 29 | 30 | ``` 31 | $ wasmtime -S http target/wasm32-wasip2/debug/http-client.wasm 32 | 33 | GET https://httpbin.org/get, status code: 200, body: 34 | { 35 | "args": { 36 | "a": "b" 37 | }, 38 | "headers": { 39 | "Accept": "*/*", 40 | "Content-Type": "application/json", 41 | "Host": "httpbin.org", 42 | "X-Amzn-Trace-Id": "..." 43 | }, 44 | "origin": "...", 45 | "url": "https://httpbin.org/get?a=b" 46 | } 47 | 48 | GET https://httpbin.org/get, status code: 200, body: 49 | Data { origin: "117.172.222.76", url: "https://httpbin.org/get" } 50 | 51 | GET https://httpbin.org/range, status code: 200, body: 52 | abcdefghij 53 | klmnopqrst 54 | 55 | POST https://httpbin.org/post, status code: 200, body: 56 | { 57 | "args": {}, 58 | "data": "{\"data\":\"hello\"}", 59 | "files": {}, 60 | "form": {}, 61 | "headers": { 62 | "Content-Length": "16", 63 | "Content-Type": "application/json", 64 | "Host": "httpbin.org", 65 | "X-Amzn-Trace-Id": "..." 66 | }, 67 | "json": { 68 | "data": "hello" 69 | }, 70 | "origin": "...", 71 | "url": "https://httpbin.org/post" 72 | } 73 | 74 | POST https://httpbin.org/post, status code: 200, body: 75 | { 76 | "args": {}, 77 | "data": "", 78 | "files": {}, 79 | "form": { 80 | "a": "b", 81 | "c": "" 82 | }, 83 | "headers": { 84 | "Content-Length": "6", 85 | "Content-Type": "application/x-www-form-urlencoded", 86 | "Host": "httpbin.org", 87 | "X-Amzn-Trace-Id": "..." 88 | }, 89 | "json": null, 90 | "origin": "...", 91 | "url": "https://httpbin.org/post" 92 | } 93 | 94 | POST https://httpbin.org/post, status code: 200, body: 95 | { 96 | "args": {}, 97 | "data": "", 98 | "files": { 99 | "field2": "hello" 100 | }, 101 | "form": { 102 | "field1": "value1" 103 | }, 104 | "headers": { 105 | "Content-Length": "181", 106 | "Content-Type": "multipart/form-data; boundary=boundary", 107 | "Host": "httpbin.org", 108 | "X-Amzn-Trace-Id": "..." 109 | }, 110 | "json": null, 111 | "origin": "...", 112 | "url": "https://httpbin.org/post" 113 | } 114 | ``` 115 | -------------------------------------------------------------------------------- /examples/client/src/main.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::collections::HashMap; 3 | use std::time::Duration; 4 | use waki::{ 5 | multipart::{Form, Part}, 6 | Client, 7 | }; 8 | 9 | #[derive(Debug, Deserialize)] 10 | #[allow(dead_code)] 11 | struct Data { 12 | origin: String, 13 | url: String, 14 | } 15 | 16 | fn main() { 17 | // get with query 18 | let resp = Client::new() 19 | .get("https://httpbin.org/get?a=b") 20 | .headers([("Content-Type", "application/json"), ("Accept", "*/*")]) 21 | .send() 22 | .unwrap(); 23 | println!( 24 | "GET https://httpbin.org/get, status code: {}, body:\n{}", 25 | resp.status_code(), 26 | String::from_utf8(resp.body().unwrap()).unwrap() 27 | ); 28 | 29 | // get with json response 30 | let resp = Client::new().get("https://httpbin.org/get").send().unwrap(); 31 | let status = resp.status_code(); 32 | let json_data = resp.json::().unwrap(); 33 | println!( 34 | "GET https://httpbin.org/get, status code: {}, body:\n{:?}\n", 35 | status, json_data, 36 | ); 37 | 38 | // play with the response chunk 39 | let resp = Client::new() 40 | .get("https://httpbin.org/range/20") 41 | .query(&[("duration", "5"), ("chunk_size", "10")]) 42 | .send() 43 | .unwrap(); 44 | println!( 45 | "GET https://httpbin.org/range, status code: {}, body:", 46 | resp.status_code() 47 | ); 48 | while let Some(chunk) = resp.chunk(1024).unwrap() { 49 | println!("{}", String::from_utf8(chunk).unwrap()); 50 | } 51 | println!(); 52 | 53 | // post with json data 54 | let resp = Client::new() 55 | .post("https://httpbin.org/post") 56 | .json(&HashMap::from([("data", "hello")])) 57 | .connect_timeout(Duration::from_secs(5)) 58 | .send() 59 | .unwrap(); 60 | println!( 61 | "POST https://httpbin.org/post, status code: {}, body:\n{}", 62 | resp.status_code(), 63 | String::from_utf8(resp.body().unwrap()).unwrap() 64 | ); 65 | 66 | // post with form data 67 | let resp = Client::new() 68 | .post("https://httpbin.org/post") 69 | .form(&[("a", "b"), ("c", "")]) 70 | .connect_timeout(Duration::from_secs(5)) 71 | .send() 72 | .unwrap(); 73 | println!( 74 | "POST https://httpbin.org/post, status code: {}, body:\n{}", 75 | resp.status_code(), 76 | String::from_utf8(resp.body().unwrap()).unwrap() 77 | ); 78 | 79 | // post with file form data 80 | let resp = Client::new() 81 | .post("https://httpbin.org/post") 82 | .multipart( 83 | Form::new().text("field1", "value1").part( 84 | Part::new("field2", "hello") 85 | .filename("file.txt") 86 | .mime_str("text/plain") 87 | .unwrap(), 88 | ), 89 | ) 90 | .connect_timeout(Duration::from_secs(5)) 91 | .send() 92 | .unwrap(); 93 | println!( 94 | "POST https://httpbin.org/post, status code: {}, body:\n{}", 95 | resp.status_code(), 96 | String::from_utf8(resp.body().unwrap()).unwrap() 97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /examples/server/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-wasip2" 3 | -------------------------------------------------------------------------------- /examples/server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "http-server" 3 | version = "0.1.0" 4 | edition = "2021" 5 | rust-version = "1.82" 6 | publish = false 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | waki = "0.4.2" 13 | 14 | # reduce wasm binary size 15 | [profile.release] 16 | lto = true 17 | strip = "symbols" 18 | -------------------------------------------------------------------------------- /examples/server/README.md: -------------------------------------------------------------------------------- 1 | # http-server 2 | 3 | This example is built using the [WASI Preview 2 API](https://github.com/WebAssembly/wasi-http) 4 | with the [waki](https://github.com/wacker-dev/waki) library. 5 | 6 | ## Build 7 | 8 | Requires Rust 1.82+. 9 | 10 | ``` 11 | $ rustup target add wasm32-wasip2 12 | ``` 13 | 14 | Use the following command to compile it into a WASM program: 15 | 16 | ``` 17 | $ cargo build 18 | ``` 19 | 20 | Or use `--release` option to build it in release mode: 21 | 22 | ``` 23 | $ cargo build --release 24 | ``` 25 | 26 | ## Run 27 | 28 | After compilation, you can use [wasmtime](https://github.com/bytecodealliance/wasmtime) to run it: 29 | 30 | ``` 31 | $ wasmtime serve -S cli target/wasm32-wasip2/debug/http_server.wasm 32 | Serving HTTP on http://0.0.0.0:8080/ 33 | ``` 34 | 35 | ``` 36 | $ curl http://localhost:8080/ 37 | Hello, WASI! 38 | 39 | $ curl http://localhost:8080/?name=ia 40 | Hello, ia! 41 | ``` 42 | -------------------------------------------------------------------------------- /examples/server/src/lib.rs: -------------------------------------------------------------------------------- 1 | use waki::{handler, ErrorCode, Request, Response}; 2 | 3 | #[handler] 4 | fn hello(req: Request) -> Result { 5 | let query = req.query(); 6 | Response::builder() 7 | .body(format!( 8 | "Hello, {}!", 9 | query.get("name").unwrap_or(&"WASI".to_string()) 10 | )) 11 | .build() 12 | } 13 | -------------------------------------------------------------------------------- /scripts/vendor-wit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Script to re-vendor the WIT files. 4 | # 5 | # This script is also executed on CI to ensure that everything is up-to-date. 6 | set -ex 7 | 8 | # Space-separated list of wasi proposals that are vendored here along with the 9 | # tag that they're all vendored at. 10 | # 11 | # This assumes that the repositories all have the pattern: 12 | # https://github.com/WebAssembly/wasi-$repo 13 | # and every repository has a tag `v$tag` here. That is currently done as part 14 | # of the WASI release process. 15 | repos="cli clocks filesystem http io random sockets" 16 | tag=0.2.2 17 | dst=waki/wit/deps 18 | 19 | rm -rf $dst 20 | mkdir -p $dst 21 | 22 | for repo in $repos; do 23 | mkdir $dst/$repo 24 | curl -L https://github.com/WebAssembly/wasi-$repo/archive/refs/tags/v$tag.tar.gz | \ 25 | tar xzf - --strip-components=2 -C $dst/$repo wasi-$repo-$tag/wit 26 | rm -rf $dst/$repo/deps* 27 | done 28 | -------------------------------------------------------------------------------- /test-programs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-programs" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | waki = { path = "../waki", features = ["json", "multipart"] } 9 | serde = { workspace = true, features = ["derive"] } 10 | mime = "0.3.17" 11 | -------------------------------------------------------------------------------- /test-programs/artifacts/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-programs-artifacts" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [build-dependencies] 8 | anyhow.workspace = true 9 | cargo_metadata = "0.18.1" 10 | wit-component = "0.219.1" 11 | heck = "0.5.0" 12 | wasi-preview1-component-adapter-provider = "25.0.1" 13 | version_check = "0.9.5" 14 | -------------------------------------------------------------------------------- /test-programs/artifacts/build.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use cargo_metadata::MetadataCommand; 3 | use heck::*; 4 | use std::path::{Path, PathBuf}; 5 | use std::process::Command; 6 | use std::{env, fs}; 7 | use wit_component::ComponentEncoder; 8 | 9 | fn main() -> Result<()> { 10 | let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); 11 | 12 | println!("cargo::rerun-if-changed=../src"); 13 | println!("cargo::rerun-if-changed=../../waki"); 14 | 15 | let wasip2 = version_check::is_min_version("1.82.0").unwrap_or(false); 16 | let wasi_target = if wasip2 { 17 | "wasm32-wasip2" 18 | } else { 19 | "wasm32-wasip1" 20 | }; 21 | 22 | let status = Command::new("cargo") 23 | .arg("build") 24 | .arg("--package=test-programs") 25 | .arg(format!("--target={wasi_target}")) 26 | .env("CARGO_TARGET_DIR", &out_dir) 27 | .env("CARGO_PROFILE_DEV_DEBUG", "1") 28 | .status()?; 29 | assert!(status.success()); 30 | 31 | let meta = MetadataCommand::new().exec()?; 32 | let targets = meta 33 | .packages 34 | .iter() 35 | .find(|p| p.name == "test-programs") 36 | .unwrap() 37 | .targets 38 | .iter() 39 | .filter(|t| t.kind == ["bin"]) 40 | .map(|t| &t.name) 41 | .collect::>(); 42 | 43 | let mut generated_code = String::new(); 44 | 45 | for target in targets { 46 | let camel = target.to_shouty_snake_case(); 47 | let wasm = out_dir 48 | .join(wasi_target) 49 | .join("debug") 50 | .join(format!("{target}.wasm")); 51 | 52 | let path = if wasip2 { 53 | wasm 54 | } else { 55 | compile_component( 56 | &wasm, 57 | match target.as_str() { 58 | s if s.starts_with("client_") => { 59 | wasi_preview1_component_adapter_provider::WASI_SNAPSHOT_PREVIEW1_COMMAND_ADAPTER 60 | } 61 | s if s.starts_with("server_") => { 62 | wasi_preview1_component_adapter_provider::WASI_SNAPSHOT_PREVIEW1_PROXY_ADAPTER 63 | } 64 | other => panic!("unknown type {other}"), 65 | }, 66 | )? 67 | }; 68 | generated_code += &format!("pub const {camel}_COMPONENT: &str = {path:?};\n"); 69 | } 70 | 71 | fs::write(out_dir.join("gen.rs"), generated_code)?; 72 | 73 | Ok(()) 74 | } 75 | 76 | // Compile a component, return the path of the binary 77 | fn compile_component(wasm: &Path, adapter: &[u8]) -> Result { 78 | let module = fs::read(wasm)?; 79 | let component = ComponentEncoder::default() 80 | .module(module.as_slice())? 81 | .validate(true) 82 | .adapter("wasi_snapshot_preview1", adapter)? 83 | .encode()?; 84 | let out_dir = wasm.parent().unwrap(); 85 | let stem = wasm.file_stem().unwrap().to_str().unwrap(); 86 | let component_path = out_dir.join(format!("{stem}.component.wasm")); 87 | fs::write(&component_path, component)?; 88 | Ok(component_path) 89 | } 90 | -------------------------------------------------------------------------------- /test-programs/artifacts/src/lib.rs: -------------------------------------------------------------------------------- 1 | include!(concat!(env!("OUT_DIR"), "/gen.rs")); 2 | -------------------------------------------------------------------------------- /test-programs/src/bin/client_get_chunk.rs: -------------------------------------------------------------------------------- 1 | use waki::Client; 2 | 3 | fn main() { 4 | let resp = Client::new() 5 | .get("https://httpbin.org/range/20") 6 | .query(&[("duration", "5"), ("chunk_size", "10")]) 7 | .send() 8 | .unwrap(); 9 | assert_eq!(resp.status_code(), 200); 10 | 11 | while let Some(chunk) = resp.chunk(1024).unwrap() { 12 | assert_eq!(String::from_utf8(chunk).unwrap().len(), 10); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test-programs/src/bin/client_get_headers.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::collections::HashMap; 3 | use waki::{header::CONTENT_TYPE, Client}; 4 | 5 | #[derive(Deserialize)] 6 | struct Data { 7 | headers: HashMap, 8 | } 9 | 10 | fn main() { 11 | let resp = Client::new() 12 | .get("https://httpbin.org/get") 13 | .header("Test", "test") 14 | .headers([("A", "b"), ("C", "d")]) 15 | .send() 16 | .unwrap(); 17 | assert_eq!(resp.status_code(), 200); 18 | assert_eq!( 19 | resp.header(CONTENT_TYPE).unwrap().to_str().unwrap(), 20 | "application/json" 21 | ); 22 | 23 | let data = resp.json::().unwrap(); 24 | assert_eq!(data.headers.get("Test").unwrap(), "test"); 25 | assert_eq!(data.headers.get("A").unwrap(), "b"); 26 | assert_eq!(data.headers.get("C").unwrap(), "d"); 27 | } 28 | -------------------------------------------------------------------------------- /test-programs/src/bin/client_get_with_query.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::collections::HashMap; 3 | use waki::Client; 4 | 5 | #[derive(Deserialize)] 6 | struct Data { 7 | args: HashMap, 8 | } 9 | 10 | fn main() { 11 | let resp = Client::new() 12 | .get("https://httpbin.org/get?a=b") 13 | .query(&[("c", "d")]) 14 | .send() 15 | .unwrap(); 16 | assert_eq!(resp.status_code(), 200); 17 | 18 | let data = resp.json::().unwrap(); 19 | assert_eq!(data.args.get("a").unwrap(), "b"); 20 | assert_eq!(data.args.get("c").unwrap(), "d"); 21 | } 22 | -------------------------------------------------------------------------------- /test-programs/src/bin/client_post_with_body.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::time::Duration; 3 | use waki::Client; 4 | 5 | #[derive(Deserialize)] 6 | struct Data { 7 | data: String, 8 | } 9 | 10 | fn main() { 11 | // Make sure the final body is larger than 1024*1024, but we cannot allocate 12 | // so much memory directly in the wasm program, so we use the `repeat` 13 | // method to increase the body size. 14 | const LEN: usize = 1024; 15 | const REPEAT: usize = 1025; 16 | let buffer = [0; LEN]; 17 | let resp = Client::new() 18 | .post("https://httpbin.org/post") 19 | .body(buffer.repeat(REPEAT)) 20 | .connect_timeout(Duration::from_secs(5)) 21 | .send() 22 | .unwrap(); 23 | assert_eq!(resp.status_code(), 200); 24 | 25 | let data = resp.json::().unwrap(); 26 | assert_eq!(data.data.len(), LEN * REPEAT); 27 | } 28 | -------------------------------------------------------------------------------- /test-programs/src/bin/client_post_with_form_data.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::collections::HashMap; 3 | use std::time::Duration; 4 | use waki::Client; 5 | 6 | #[derive(Deserialize)] 7 | struct Data { 8 | form: HashMap, 9 | } 10 | 11 | fn main() { 12 | let resp = Client::new() 13 | .post("https://httpbin.org/post") 14 | .form(&[("a", "b"), ("c", "")]) 15 | .connect_timeout(Duration::from_secs(5)) 16 | .send() 17 | .unwrap(); 18 | assert_eq!(resp.status_code(), 200); 19 | 20 | let data = resp.json::().unwrap(); 21 | assert_eq!(data.form.get("a").unwrap(), "b"); 22 | assert_eq!(data.form.get("c").unwrap(), ""); 23 | } 24 | -------------------------------------------------------------------------------- /test-programs/src/bin/client_post_with_json_data.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::collections::HashMap; 3 | use std::time::Duration; 4 | use waki::Client; 5 | 6 | #[derive(Deserialize)] 7 | struct Data { 8 | json: HashMap, 9 | } 10 | 11 | fn main() { 12 | let resp = Client::new() 13 | .post("https://httpbin.org/post") 14 | .json(&HashMap::from([("data", "hello")])) 15 | .connect_timeout(Duration::from_secs(5)) 16 | .send() 17 | .unwrap(); 18 | assert_eq!(resp.status_code(), 200); 19 | 20 | let data = resp.json::().unwrap(); 21 | assert_eq!(data.json.get("data").unwrap(), "hello"); 22 | } 23 | -------------------------------------------------------------------------------- /test-programs/src/bin/client_post_with_multipart_form_data.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::collections::HashMap; 3 | use std::time::Duration; 4 | use waki::{ 5 | multipart::{Form, Part}, 6 | Client, 7 | }; 8 | 9 | #[derive(Deserialize)] 10 | struct Data { 11 | form: HashMap, 12 | files: HashMap, 13 | } 14 | 15 | fn main() { 16 | let resp = Client::new() 17 | .post("https://httpbin.org/post") 18 | .multipart( 19 | Form::new() 20 | .text("field1", "value1") 21 | .file("field2", "file.txt") 22 | .unwrap() 23 | .part( 24 | Part::new("field3", "hello") 25 | .filename("file.txt") 26 | .mime(mime::TEXT_PLAIN), 27 | ), 28 | ) 29 | .connect_timeout(Duration::from_secs(5)) 30 | .send() 31 | .unwrap(); 32 | assert_eq!(resp.status_code(), 200); 33 | 34 | let data = resp.json::().unwrap(); 35 | assert_eq!(data.form.get("field1").unwrap(), "value1"); 36 | assert_eq!(data.files.get("field2").unwrap(), "hello\n"); 37 | assert_eq!(data.files.get("field3").unwrap(), "hello"); 38 | } 39 | -------------------------------------------------------------------------------- /test-programs/src/bin/server_authority.rs: -------------------------------------------------------------------------------- 1 | use waki::{handler, ErrorCode, Request, Response}; 2 | 3 | #[handler] 4 | fn hello(req: Request) -> Result { 5 | let authority = req.authority(); 6 | 7 | match authority { 8 | Some(authority) => Response::builder() 9 | .body(format!("Hello, {}!", authority.as_str())) 10 | .build(), 11 | None => Response::builder().body("Hello!").build(), 12 | } 13 | } 14 | 15 | // required since this file is built as a `bin` 16 | fn main() {} 17 | -------------------------------------------------------------------------------- /test-programs/src/bin/server_form.rs: -------------------------------------------------------------------------------- 1 | use waki::{handler, ErrorCode, Request, Response}; 2 | 3 | #[handler] 4 | fn hello(req: Request) -> Result { 5 | let form = req.form().unwrap(); 6 | Response::builder() 7 | .body(format!( 8 | "{} {}", 9 | form.get("key1").unwrap(), 10 | form.get("key2").unwrap() 11 | )) 12 | .build() 13 | } 14 | 15 | // required since this file is built as a `bin` 16 | fn main() {} 17 | -------------------------------------------------------------------------------- /test-programs/src/bin/server_json.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use waki::{handler, ErrorCode, Request, Response}; 3 | 4 | #[handler] 5 | fn hello(req: Request) -> Result { 6 | let json = req.json::>().unwrap(); 7 | Response::builder() 8 | .body(json.get("data").unwrap().to_string()) 9 | .build() 10 | } 11 | 12 | // required since this file is built as a `bin` 13 | fn main() {} 14 | -------------------------------------------------------------------------------- /test-programs/src/bin/server_large_body.rs: -------------------------------------------------------------------------------- 1 | use waki::{handler, ErrorCode, Request, Response}; 2 | 3 | #[handler] 4 | fn hello(_: Request) -> Result { 5 | let buffer = [0; 5000]; 6 | Response::builder().body(buffer).build() 7 | } 8 | 9 | // required since this file is built as a `bin` 10 | fn main() {} 11 | -------------------------------------------------------------------------------- /test-programs/src/bin/server_multipart_form.rs: -------------------------------------------------------------------------------- 1 | use waki::{handler, ErrorCode, Request, Response}; 2 | 3 | #[handler] 4 | fn hello(req: Request) -> Result { 5 | let form = req.multipart().unwrap(); 6 | let form_part = form.get("form").unwrap(); 7 | let file_part = form.get("file").unwrap(); 8 | Response::builder() 9 | .body([form_part.value.as_slice(), file_part.value.as_slice()].concat()) 10 | .build() 11 | } 12 | 13 | // required since this file is built as a `bin` 14 | fn main() {} 15 | -------------------------------------------------------------------------------- /test-programs/src/bin/server_query.rs: -------------------------------------------------------------------------------- 1 | use waki::{handler, ErrorCode, Request, Response}; 2 | 3 | #[handler] 4 | fn hello(req: Request) -> Result { 5 | let query = req.query(); 6 | Response::builder() 7 | .body(format!( 8 | "Hello, {}!", 9 | query.get("name").unwrap_or(&"WASI".to_string()) 10 | )) 11 | .build() 12 | } 13 | 14 | // required since this file is built as a `bin` 15 | fn main() {} 16 | -------------------------------------------------------------------------------- /test-programs/src/bin/server_status_code.rs: -------------------------------------------------------------------------------- 1 | use waki::{handler, ErrorCode, Request, Response}; 2 | 3 | #[handler] 4 | fn hello(_: Request) -> Result { 5 | Response::builder().status_code(400).build() 6 | } 7 | 8 | // required since this file is built as a `bin` 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /waki-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "waki-macros" 3 | readme.workspace = true 4 | description.workspace = true 5 | version.workspace = true 6 | authors.workspace = true 7 | edition.workspace = true 8 | categories.workspace = true 9 | keywords.workspace = true 10 | repository.workspace = true 11 | license.workspace = true 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | syn = { version = "2.0.77", features = ["full"] } 18 | quote = "1.0.37" 19 | proc-macro2 = "1.0.86" 20 | -------------------------------------------------------------------------------- /waki-macros/src/dummy.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | 4 | pub fn wrap_in_const(code: TokenStream) -> TokenStream { 5 | quote! { 6 | #[doc(hidden)] 7 | const _: () = { 8 | #code 9 | }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /waki-macros/src/export.rs: -------------------------------------------------------------------------------- 1 | use crate::dummy; 2 | 3 | use proc_macro2::TokenStream; 4 | use quote::quote; 5 | use syn::{ItemFn, Result}; 6 | 7 | pub fn handler(input: ItemFn) -> Result { 8 | let fn_name = &input.sig.ident; 9 | 10 | Ok(dummy::wrap_in_const(quote! { 11 | #input 12 | 13 | struct Component; 14 | 15 | ::waki::bindings::export!(Component with_types_in ::waki::bindings); 16 | 17 | impl ::waki::bindings::exports::wasi::http::incoming_handler::Guest for Component { 18 | fn handle(request: ::waki::bindings::wasi::http::types::IncomingRequest, response_out: ::waki::bindings::wasi::http::types::ResponseOutparam) { 19 | match request.try_into() { 20 | Ok(req) => match #fn_name(req) { 21 | Ok(resp) => ::waki::handle_response(response_out, resp), 22 | Err(e) => ::waki::bindings::wasi::http::types::ResponseOutparam::set(response_out, Err(e)), 23 | } 24 | Err(e) => ::waki::bindings::wasi::http::types::ResponseOutparam::set(response_out, Err(e)), 25 | } 26 | } 27 | } 28 | })) 29 | } 30 | -------------------------------------------------------------------------------- /waki-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod dummy; 2 | mod export; 3 | 4 | use proc_macro::TokenStream; 5 | use syn::{parse_macro_input, ItemFn}; 6 | 7 | #[proc_macro_attribute] 8 | pub fn handler(_: TokenStream, input: TokenStream) -> TokenStream { 9 | export::handler(parse_macro_input!(input as ItemFn)) 10 | .unwrap_or_else(syn::Error::into_compile_error) 11 | .into() 12 | } 13 | -------------------------------------------------------------------------------- /waki/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "waki" 3 | readme.workspace = true 4 | description.workspace = true 5 | version.workspace = true 6 | authors.workspace = true 7 | edition.workspace = true 8 | categories.workspace = true 9 | keywords.workspace = true 10 | repository.workspace = true 11 | license.workspace = true 12 | 13 | [package.metadata.docs.rs] 14 | all-features = true 15 | 16 | [dependencies] 17 | waki-macros.workspace = true 18 | 19 | anyhow.workspace = true 20 | serde.workspace = true 21 | wit-bindgen = "0.34.0" 22 | form_urlencoded = "1.2.1" 23 | http = "1.1.0" 24 | serde_json = { version = "1.0.128", optional = true } 25 | mime = { version = "0.3.17", optional = true } 26 | mime_guess = { version = "2.0.5", optional = true } 27 | rand = { version = "0.8.5", optional = true } 28 | memchr = { version = "2.7.4", optional = true } 29 | bytes = { version = "1.7.2", optional = true } 30 | httparse = { version = "1.9.4", optional = true } 31 | 32 | [features] 33 | json = ["dep:serde_json"] 34 | multipart = ["dep:mime", "dep:mime_guess", "dep:rand", "dep:memchr", "dep:bytes", "dep:httparse"] 35 | 36 | [dev-dependencies] 37 | test-programs-artifacts = { path = "../test-programs/artifacts" } 38 | 39 | wasmtime = "25.0.0" 40 | wasmtime-wasi = "25.0.0" 41 | wasmtime-wasi-http = "25.0.0" 42 | hyper = "1.4.1" 43 | http-body-util = "0.1.2" 44 | tokio = { version = "1.40.0", features = ["macros"] } 45 | -------------------------------------------------------------------------------- /waki/src/body.rs: -------------------------------------------------------------------------------- 1 | use crate::bindings::wasi::{ 2 | http::types::{IncomingBody, InputStream, OutgoingBody}, 3 | io::streams::StreamError, 4 | }; 5 | 6 | use anyhow::{anyhow, Result}; 7 | 8 | pub struct IncomingBodyStream { 9 | // input-stream resource is a child: it must be dropped before the parent incoming-body is dropped 10 | input_stream: InputStream, 11 | _incoming_body: IncomingBody, 12 | } 13 | 14 | impl From for IncomingBodyStream { 15 | #[inline] 16 | fn from(body: IncomingBody) -> Self { 17 | Self { 18 | // The stream() method can only be called once 19 | input_stream: body.stream().unwrap(), 20 | _incoming_body: body, 21 | } 22 | } 23 | } 24 | 25 | impl InputStream { 26 | pub fn chunk(&self, len: u64) -> Result>> { 27 | match self.blocking_read(len) { 28 | Ok(c) => Ok(Some(c)), 29 | Err(StreamError::Closed) => Ok(None), 30 | Err(e) => Err(anyhow!("input_stream read failed: {e:?}"))?, 31 | } 32 | } 33 | } 34 | 35 | pub enum Body { 36 | Bytes(Vec), 37 | Stream(IncomingBodyStream), 38 | } 39 | 40 | impl Body { 41 | #[inline] 42 | pub fn chunk(&self, len: u64) -> Result>> { 43 | match &self { 44 | Body::Bytes(_) => Ok(None), 45 | Body::Stream(s) => s.input_stream.chunk(len), 46 | } 47 | } 48 | 49 | pub fn bytes(self) -> Result> { 50 | match self { 51 | Body::Bytes(data) => Ok(data), 52 | Body::Stream(s) => { 53 | let mut body = Vec::new(); 54 | while let Some(mut chunk) = s.input_stream.chunk(1024 * 1024)? { 55 | body.append(&mut chunk); 56 | } 57 | Ok(body) 58 | } 59 | } 60 | } 61 | } 62 | 63 | pub(crate) fn write_to_outgoing_body(outgoing_body: &OutgoingBody, mut buf: &[u8]) -> Result<()> { 64 | if buf.is_empty() { 65 | return Ok(()); 66 | } 67 | 68 | let out = outgoing_body 69 | .write() 70 | .map_err(|_| anyhow!("outgoing request write failed"))?; 71 | 72 | let pollable = out.subscribe(); 73 | while !buf.is_empty() { 74 | pollable.block(); 75 | 76 | let permit = out.check_write()?; 77 | let len = buf.len().min(permit as usize); 78 | let (chunk, rest) = buf.split_at(len); 79 | buf = rest; 80 | 81 | out.write(chunk)?; 82 | } 83 | 84 | out.flush()?; 85 | pollable.block(); 86 | let _ = out.check_write()?; 87 | Ok(()) 88 | } 89 | -------------------------------------------------------------------------------- /waki/src/client.rs: -------------------------------------------------------------------------------- 1 | use crate::{Method, RequestBuilder}; 2 | 3 | #[derive(Default)] 4 | pub struct Client {} 5 | 6 | impl Client { 7 | #[inline] 8 | pub fn new() -> Self { 9 | Default::default() 10 | } 11 | 12 | #[inline] 13 | pub fn get(&self, url: &str) -> RequestBuilder { 14 | self.request(Method::Get, url) 15 | } 16 | 17 | #[inline] 18 | pub fn post(&self, url: &str) -> RequestBuilder { 19 | self.request(Method::Post, url) 20 | } 21 | 22 | #[inline] 23 | pub fn put(&self, url: &str) -> RequestBuilder { 24 | self.request(Method::Put, url) 25 | } 26 | 27 | #[inline] 28 | pub fn patch(&self, url: &str) -> RequestBuilder { 29 | self.request(Method::Patch, url) 30 | } 31 | 32 | #[inline] 33 | pub fn delete(&self, url: &str) -> RequestBuilder { 34 | self.request(Method::Delete, url) 35 | } 36 | 37 | #[inline] 38 | pub fn head(&self, url: &str) -> RequestBuilder { 39 | self.request(Method::Head, url) 40 | } 41 | 42 | #[inline] 43 | pub fn request(&self, method: Method, url: &str) -> RequestBuilder { 44 | RequestBuilder::new(method, url) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /waki/src/common/header.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | bindings::wasi::http::types::{HeaderError, Headers, IncomingRequest, IncomingResponse}, 3 | header::HeaderMap, 4 | }; 5 | use anyhow::Result; 6 | 7 | macro_rules! impl_header { 8 | ($($t:ty),+ $(,)?) => ($( 9 | impl $t { 10 | pub fn headers_map(&self) -> Result { 11 | let headers_handle = self.headers(); 12 | headers_handle 13 | .entries() 14 | .into_iter() 15 | .map(|(key, value)| Ok((key.try_into()?, value.try_into()?))) 16 | .collect::>() 17 | } 18 | } 19 | )+) 20 | } 21 | 22 | impl_header!(IncomingRequest, IncomingResponse); 23 | 24 | impl TryFrom for Headers { 25 | type Error = HeaderError; 26 | 27 | fn try_from(headers: HeaderMap) -> Result { 28 | let entries = headers 29 | .iter() 30 | .map(|(k, v)| (k.to_string(), v.as_bytes().into())) 31 | .collect::>(); 32 | Headers::from_list(&entries) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /waki/src/common/mod.rs: -------------------------------------------------------------------------------- 1 | mod header; 2 | mod request_and_response; 3 | mod scheme; 4 | -------------------------------------------------------------------------------- /waki/src/common/request_and_response.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "multipart")] 2 | use crate::multipart::{parser::parse, Form, Part}; 3 | use crate::{ 4 | body::Body, 5 | header::{AsHeaderName, HeaderMap, HeaderValue, IntoHeaderName, CONTENT_TYPE}, 6 | Request, RequestBuilder, Response, ResponseBuilder, 7 | }; 8 | use anyhow::{anyhow, Error, Result}; 9 | use serde::Serialize; 10 | use std::borrow::Borrow; 11 | use std::collections::HashMap; 12 | 13 | macro_rules! impl_common_get_methods { 14 | ($($t:ty),+ $(,)?) => ($( 15 | impl $t { 16 | /// Get the header. 17 | #[inline] 18 | pub fn header(&self, key: K) -> Option<&HeaderValue> { 19 | self.headers.get(key) 20 | } 21 | 22 | /// Get headers. 23 | #[inline] 24 | pub fn headers(&self) -> &HeaderMap { 25 | &self.headers 26 | } 27 | 28 | /// Get a chunk of the body. 29 | /// 30 | /// It will block until at least one byte can be read or the stream is closed. 31 | /// 32 | /// NOTE: This method is only for incoming requests/responses, if you call it on an 33 | /// outgoing request/response it will always return None. 34 | #[inline] 35 | pub fn chunk(&self, len: u64) -> Result>> { 36 | self.body.chunk(len) 37 | } 38 | 39 | /// Get the full body. 40 | /// 41 | /// It will block until the stream is closed. 42 | #[inline] 43 | pub fn body(self) -> Result> { 44 | self.body.bytes() 45 | } 46 | 47 | /// Deserialize the body as JSON. 48 | /// 49 | /// # Optional 50 | /// 51 | /// This requires the `json` feature enabled. 52 | /// 53 | /// ``` 54 | /// # use anyhow::Result; 55 | /// # use serde::Deserialize; 56 | /// # use waki::Response; 57 | /// # fn run() -> Result<()> { 58 | /// # let r = Response::new(); 59 | /// #[derive(Deserialize)] 60 | /// struct Data { 61 | /// origin: String, 62 | /// url: String, 63 | /// } 64 | /// 65 | /// let json_data = r.json::()?; 66 | /// # Ok(()) 67 | /// # } 68 | /// ``` 69 | #[cfg(feature = "json")] 70 | pub fn json(self) -> Result { 71 | Ok(serde_json::from_slice(self.body()?.as_ref())?) 72 | } 73 | 74 | /// Parse the body as form data. 75 | pub fn form(self) -> Result> { 76 | Ok(form_urlencoded::parse(self.body()?.as_ref()).into_owned().collect()) 77 | } 78 | 79 | /// Parse the body as multipart/form-data. 80 | /// 81 | /// # Optional 82 | /// 83 | /// This requires the `multipart` feature enabled. 84 | #[cfg(feature = "multipart")] 85 | pub fn multipart(self) -> Result> { 86 | match self.headers.get(CONTENT_TYPE) { 87 | Some(header) => { 88 | let mime = header.to_str()?.parse::()?; 89 | let boundary = match mime.get_param(mime::BOUNDARY) { 90 | Some(v) => v.as_str(), 91 | None => { 92 | return Err(anyhow!( 93 | "unable to find the boundary value in the Content-Type header" 94 | )) 95 | } 96 | }; 97 | parse(self.body()?.as_ref(), boundary) 98 | } 99 | None => Err(anyhow!( 100 | "parse body as multipart failed, unable to find the Content-Type header" 101 | )), 102 | } 103 | } 104 | } 105 | )+) 106 | } 107 | 108 | impl_common_get_methods!(Request, Response); 109 | 110 | macro_rules! impl_common_set_methods { 111 | ($($t:ty),+ $(,)?) => ($( 112 | impl $t { 113 | /// Add a header. 114 | /// 115 | /// ``` 116 | /// # use waki::ResponseBuilder; 117 | /// # fn run() { 118 | /// # let r = ResponseBuilder::new(); 119 | /// r.header("Content-Type", "application/json"); 120 | /// # } 121 | /// ``` 122 | pub fn header(mut self, key: K, value: V) -> Self 123 | where 124 | K: IntoHeaderName, 125 | V: TryInto, 126 | >::Error: Into, 127 | { 128 | let mut err = None; 129 | if let Ok(ref mut inner) = self.inner { 130 | match value.try_into().map_err(|e| e.into()) { 131 | Ok(v) => { 132 | inner.headers.insert(key, v); 133 | } 134 | Err(e) => err = Some(e), 135 | }; 136 | } 137 | if let Some(e) = err { 138 | self.inner = Err(e); 139 | } 140 | self 141 | } 142 | 143 | /// Add a set of headers. 144 | /// 145 | /// ``` 146 | /// # use waki::ResponseBuilder; 147 | /// # fn run() { 148 | /// # let r = ResponseBuilder::new(); 149 | /// r.headers([("Content-Type", "application/json"), ("Accept", "*/*")]); 150 | /// # } 151 | /// ``` 152 | pub fn headers(mut self, headers: I) -> Self 153 | where 154 | K: IntoHeaderName, 155 | V: TryInto, 156 | >::Error: Into, 157 | I: IntoIterator, 158 | { 159 | let mut err = None; 160 | if let Ok(ref mut inner) = self.inner { 161 | for (key, value) in headers.into_iter() { 162 | match value.try_into().map_err(|e| e.into()) { 163 | Ok(v) => { 164 | inner.headers.insert(key, v); 165 | } 166 | Err(e) => { 167 | err = Some(e); 168 | break; 169 | } 170 | }; 171 | } 172 | } 173 | if let Some(e) = err { 174 | self.inner = Err(e); 175 | } 176 | self 177 | } 178 | 179 | /// Set the body. 180 | /// 181 | /// ``` 182 | /// # use waki::ResponseBuilder; 183 | /// # fn run() { 184 | /// # let r = ResponseBuilder::new(); 185 | /// r.body("hello"); 186 | /// # } 187 | /// ``` 188 | #[inline] 189 | pub fn body>>(mut self, body: V) -> Self { 190 | if let Ok(ref mut inner) = self.inner { 191 | inner.body = Body::Bytes(body.into()); 192 | } 193 | self 194 | } 195 | 196 | /// Set a JSON body. 197 | /// 198 | /// # Optional 199 | /// 200 | /// This requires the `json` feature enabled. 201 | /// 202 | /// ``` 203 | /// # use std::collections::HashMap; 204 | /// # use waki::ResponseBuilder; 205 | /// # fn run() { 206 | /// # let r = ResponseBuilder::new(); 207 | /// r.json(&HashMap::from([("data", "hello")])); 208 | /// # } 209 | /// ``` 210 | #[cfg(feature = "json")] 211 | pub fn json(mut self, json: &T) -> Self { 212 | let mut err = None; 213 | if let Ok(ref mut inner) = self.inner { 214 | inner.headers.insert(CONTENT_TYPE, "application/json".parse().unwrap()); 215 | match serde_json::to_vec(json) { 216 | Ok(data) => inner.body = Body::Bytes(data), 217 | Err(e) => err = Some(e.into()), 218 | } 219 | } 220 | if let Some(e) = err { 221 | self.inner = Err(e); 222 | } 223 | self 224 | } 225 | 226 | /// Set a form body. 227 | /// 228 | /// ``` 229 | /// # use waki::ResponseBuilder; 230 | /// # fn run() { 231 | /// # let r = ResponseBuilder::new(); 232 | /// r.form([("a", "b"), ("c", "d")]); 233 | /// # } 234 | /// ``` 235 | pub fn form(mut self, form: I) -> Self 236 | where 237 | K: AsRef, 238 | V: AsRef, 239 | I: IntoIterator, 240 | I::Item: Borrow<(K, V)>, 241 | { 242 | if let Ok(ref mut inner) = self.inner { 243 | inner.headers.insert( 244 | CONTENT_TYPE, 245 | "application/x-www-form-urlencoded".parse().unwrap(), 246 | ); 247 | let mut serializer = form_urlencoded::Serializer::new(String::new()); 248 | serializer.extend_pairs(form); 249 | inner.body = Body::Bytes(serializer.finish().into()) 250 | } 251 | self 252 | } 253 | 254 | /// Set a multipart/form-data body. 255 | /// 256 | /// # Optional 257 | /// 258 | /// This requires the `multipart` feature enabled. 259 | /// 260 | /// ``` 261 | /// # use anyhow::Result; 262 | /// # use waki::ResponseBuilder; 263 | /// # fn run() -> Result<()> { 264 | /// # let r = ResponseBuilder::new(); 265 | /// let form = waki::multipart::Form::new() 266 | /// // Add a text field 267 | /// .text("key", "value") 268 | /// // And a file 269 | /// .file("file", "/path/to/file.txt")? 270 | /// // And a custom part 271 | /// .part( 272 | /// waki::multipart::Part::new("key2", "value2") 273 | /// .filename("file.txt") 274 | /// .mime_str("text/plain")?, 275 | /// ); 276 | /// 277 | /// r.multipart(form); 278 | /// # Ok(()) 279 | /// # } 280 | /// ``` 281 | #[cfg(feature = "multipart")] 282 | pub fn multipart(mut self, form: Form) -> Self { 283 | if let Ok(ref mut inner) = self.inner { 284 | inner.headers.insert( 285 | CONTENT_TYPE, 286 | format!("multipart/form-data; boundary={}", form.boundary()) 287 | .parse() 288 | .unwrap(), 289 | ); 290 | inner.body = Body::Bytes(form.build()); 291 | } 292 | self 293 | } 294 | } 295 | )+) 296 | } 297 | 298 | impl_common_set_methods!(RequestBuilder, ResponseBuilder); 299 | -------------------------------------------------------------------------------- /waki/src/common/scheme.rs: -------------------------------------------------------------------------------- 1 | use crate::bindings::wasi::http::types::Scheme; 2 | 3 | impl From<&str> for Scheme { 4 | #[inline] 5 | fn from(s: &str) -> Self { 6 | match s { 7 | "http" => Scheme::Http, 8 | "https" => Scheme::Https, 9 | other => Scheme::Other(other.to_string()), 10 | } 11 | } 12 | } 13 | 14 | impl TryInto for Scheme { 15 | type Error = http::uri::InvalidUri; 16 | 17 | #[inline] 18 | fn try_into(self) -> Result { 19 | match self { 20 | Scheme::Http => Ok(http::uri::Scheme::HTTP), 21 | Scheme::Https => Ok(http::uri::Scheme::HTTPS), 22 | Scheme::Other(s) => s.as_str().try_into(), 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /waki/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # waki 2 | //! 3 | //! HTTP client and server library for WASI. 4 | //! 5 | //! Send a request: 6 | //! 7 | //! ``` 8 | //! # use anyhow::Result; 9 | //! # use std::time::Duration; 10 | //! # use waki::Client; 11 | //! # fn run() -> Result<()> { 12 | //! let resp = Client::new() 13 | //! .post("https://httpbin.org/post") 14 | //! .connect_timeout(Duration::from_secs(5)) 15 | //! .send()?; 16 | //! 17 | //! println!("status code: {}", resp.status_code()); 18 | //! # Ok(()) 19 | //! # } 20 | //! ``` 21 | //! 22 | //! Writing an HTTP component: 23 | //! 24 | //! ``` 25 | //! use waki::{handler, ErrorCode, Request, Response}; 26 | //! 27 | //! #[handler] 28 | //! fn hello(req: Request) -> Result { 29 | //! Response::builder().body(b"Hello, WASI!").build() 30 | //! } 31 | //! ``` 32 | 33 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 34 | 35 | mod body; 36 | mod client; 37 | mod common; 38 | #[cfg(feature = "multipart")] 39 | pub mod multipart; 40 | mod request; 41 | mod response; 42 | 43 | #[doc(hidden)] 44 | pub mod bindings { 45 | wit_bindgen::generate!({ 46 | path: "wit", 47 | world: "http", 48 | pub_export_macro: true, 49 | generate_all, 50 | }); 51 | } 52 | 53 | #[doc(hidden)] 54 | pub use self::response::handle_response; 55 | pub use self::{ 56 | bindings::wasi::http::types::{ErrorCode, Method}, 57 | client::Client, 58 | request::{Request, RequestBuilder}, 59 | response::{Response, ResponseBuilder}, 60 | }; 61 | 62 | /// Export the annotated function as entrypoint of the WASI HTTP component. 63 | /// 64 | /// The function needs to have one [`Request`] parameter and one Result<[`Response`], [`ErrorCode`]> return value. 65 | /// 66 | /// For example: 67 | /// 68 | /// ``` 69 | /// use waki::{handler, ErrorCode, Request, Response}; 70 | /// 71 | /// #[handler] 72 | /// fn hello(req: Request) -> Result { 73 | /// Response::builder().body(b"Hello, WASI!").build() 74 | /// } 75 | /// ``` 76 | pub use waki_macros::handler; 77 | 78 | pub use http::header; 79 | -------------------------------------------------------------------------------- /waki/src/multipart/constants.rs: -------------------------------------------------------------------------------- 1 | pub const MAX_HEADERS: usize = 32; 2 | pub const BOUNDARY_EXT: &str = "--"; 3 | pub const CRLF: &str = "\r\n"; 4 | pub const CRLF_CRLF: &str = "\r\n\r\n"; 5 | -------------------------------------------------------------------------------- /waki/src/multipart/mod.rs: -------------------------------------------------------------------------------- 1 | mod constants; 2 | pub(crate) mod parser; 3 | 4 | use crate::header::{HeaderMap, HeaderValue, IntoHeaderName, CONTENT_DISPOSITION, CONTENT_TYPE}; 5 | 6 | use anyhow::{Error, Result}; 7 | use mime::Mime; 8 | use rand::{distributions::Alphanumeric, thread_rng, Rng}; 9 | use std::fs::File; 10 | use std::io::Read; 11 | use std::path::Path; 12 | 13 | pub struct Form { 14 | parts: Vec, 15 | boundary: String, 16 | } 17 | 18 | impl Default for Form { 19 | fn default() -> Self { 20 | Self::new() 21 | } 22 | } 23 | 24 | impl Form { 25 | pub fn new() -> Self { 26 | Self { 27 | parts: vec![], 28 | boundary: format!("--FormBoundary{}", generate_random_string(10)), 29 | } 30 | } 31 | 32 | pub(crate) fn boundary(&self) -> &str { 33 | &self.boundary 34 | } 35 | 36 | pub fn text(mut self, key: S, value: V) -> Self 37 | where 38 | S: Into, 39 | V: Into>, 40 | { 41 | self.parts.push(Part::new(key, value)); 42 | self 43 | } 44 | 45 | pub fn file(mut self, key: S, path: P) -> Result 46 | where 47 | S: Into, 48 | P: AsRef, 49 | { 50 | self.parts.push(Part::file(key, path)?); 51 | Ok(self) 52 | } 53 | 54 | pub fn part(mut self, part: Part) -> Self { 55 | self.parts.push(part); 56 | self 57 | } 58 | 59 | pub fn build(self) -> Vec { 60 | let mut buf = vec![]; 61 | for part in self.parts { 62 | buf.extend_from_slice( 63 | format!( 64 | "{}{}{}{}: form-data; name={}", 65 | constants::BOUNDARY_EXT, 66 | self.boundary, 67 | constants::CRLF, 68 | CONTENT_DISPOSITION, 69 | part.key 70 | ) 71 | .as_bytes(), 72 | ); 73 | if let Some(filename) = part.filename { 74 | buf.extend_from_slice(format!("; filename=\"{}\"", filename).as_bytes()); 75 | } 76 | if let Some(mime) = part.mime { 77 | buf.extend_from_slice( 78 | format!("{}{}: {}", constants::CRLF, CONTENT_TYPE, mime).as_bytes(), 79 | ); 80 | } 81 | for (k, v) in part.headers.iter() { 82 | buf.extend_from_slice(format!("{}{}: ", constants::CRLF, k).as_bytes()); 83 | buf.extend_from_slice(v.as_bytes()); 84 | } 85 | 86 | buf.extend_from_slice(constants::CRLF_CRLF.as_bytes()); 87 | buf.extend_from_slice(&part.value); 88 | buf.extend_from_slice(constants::CRLF.as_bytes()); 89 | } 90 | buf.extend_from_slice( 91 | format!( 92 | "{}{}{}", 93 | constants::BOUNDARY_EXT, 94 | self.boundary, 95 | constants::BOUNDARY_EXT, 96 | ) 97 | .as_bytes(), 98 | ); 99 | buf 100 | } 101 | } 102 | 103 | fn generate_random_string(length: usize) -> String { 104 | thread_rng() 105 | .sample_iter(&Alphanumeric) 106 | .take(length) 107 | .map(char::from) 108 | .collect() 109 | } 110 | 111 | pub struct Part { 112 | pub key: String, 113 | pub value: Vec, 114 | pub filename: Option, 115 | pub mime: Option, 116 | pub headers: HeaderMap, 117 | } 118 | 119 | impl Part { 120 | pub fn new(key: S, value: V) -> Self 121 | where 122 | S: Into, 123 | V: Into>, 124 | { 125 | Self { 126 | key: key.into(), 127 | value: value.into(), 128 | filename: None, 129 | mime: None, 130 | headers: HeaderMap::new(), 131 | } 132 | } 133 | 134 | pub fn file(key: S, path: P) -> Result 135 | where 136 | S: Into, 137 | P: AsRef, 138 | { 139 | let path = path.as_ref(); 140 | let mime = mime_guess::from_path(path).first_or_octet_stream(); 141 | let mut file = File::open(path)?; 142 | let mut buffer = vec![]; 143 | file.read_to_end(&mut buffer)?; 144 | let part = Part::new(key, buffer).mime(mime); 145 | 146 | match path 147 | .file_name() 148 | .map(|filename| filename.to_string_lossy().to_string()) 149 | { 150 | Some(name) => Ok(part.filename(name)), 151 | None => Ok(part), 152 | } 153 | } 154 | 155 | pub fn mime(mut self, mime: Mime) -> Self { 156 | self.mime = Some(mime); 157 | self 158 | } 159 | 160 | pub fn mime_str(mut self, mime: &str) -> Result { 161 | self.mime = Some(mime.parse()?); 162 | Ok(self) 163 | } 164 | 165 | pub fn filename>(mut self, name: S) -> Self { 166 | self.filename = Some(name.into()); 167 | self 168 | } 169 | 170 | pub fn headers(mut self, headers: I) -> Result 171 | where 172 | K: IntoHeaderName, 173 | V: TryInto, 174 | >::Error: Into, 175 | I: IntoIterator, 176 | { 177 | for (key, value) in headers.into_iter() { 178 | self.headers 179 | .insert(key, value.try_into().map_err(|e| e.into())?); 180 | } 181 | Ok(self) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /waki/src/multipart/parser.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | header::{HeaderMap, HeaderName, HeaderValue, CONTENT_DISPOSITION, CONTENT_TYPE}, 3 | multipart::{constants, Part}, 4 | }; 5 | 6 | use anyhow::{anyhow, Result}; 7 | use bytes::{Buf, Bytes, BytesMut}; 8 | use httparse::Status; 9 | use std::collections::HashMap; 10 | 11 | struct Buffer { 12 | buf: BytesMut, 13 | } 14 | 15 | impl Buffer { 16 | fn new(data: &[u8]) -> Self { 17 | Self { buf: data.into() } 18 | } 19 | 20 | fn peek_exact(&mut self, size: usize) -> Option<&[u8]> { 21 | self.buf.get(..size) 22 | } 23 | 24 | fn read_until(&mut self, pattern: &[u8]) -> Option { 25 | memchr::memmem::find(&self.buf, pattern) 26 | .map(|idx| self.buf.split_to(idx + pattern.len()).freeze()) 27 | } 28 | 29 | fn read_to(&mut self, pattern: &[u8]) -> Option { 30 | memchr::memmem::find(&self.buf, pattern).map(|idx| self.buf.split_to(idx).freeze()) 31 | } 32 | 33 | fn advance(&mut self, n: usize) { 34 | self.buf.advance(n) 35 | } 36 | } 37 | 38 | pub fn parse(body: &[u8], boundary: &str) -> Result> { 39 | let mut buffer = Buffer::new(body); 40 | let boundary = format!("{}{}", constants::BOUNDARY_EXT, boundary); 41 | 42 | // Finding the first boundary 43 | if buffer 44 | .read_until(format!("{}{}", boundary, constants::CRLF).as_bytes()) 45 | .is_none() 46 | { 47 | return Err(anyhow!("incomplete multipart data, missing boundary")); 48 | }; 49 | 50 | let mut parts = HashMap::new(); 51 | 52 | loop { 53 | // Finding headers 54 | let header_bytes = match buffer.read_until(constants::CRLF_CRLF.as_bytes()) { 55 | Some(bytes) => bytes, 56 | None => return Err(anyhow!("incomplete multipart data, missing headers")), 57 | }; 58 | 59 | let mut part = Part::new("", vec![]); 60 | let mut headers = [httparse::EMPTY_HEADER; constants::MAX_HEADERS]; 61 | part.headers = match httparse::parse_headers(&header_bytes, &mut headers)? { 62 | Status::Complete((_, raw_headers)) => { 63 | let mut headers_map = HeaderMap::with_capacity(raw_headers.len()); 64 | for header in raw_headers { 65 | let (k, v) = ( 66 | HeaderName::try_from(header.name)?, 67 | HeaderValue::try_from(header.value)?, 68 | ); 69 | if k == CONTENT_DISPOSITION { 70 | // can't parse it without a / 71 | let mime = format!("multipart/{}", v.to_str()?).parse::()?; 72 | part.key = match mime.get_param("name") { 73 | Some(name) => name.to_string(), 74 | None => { 75 | return Err(anyhow!( 76 | "missing name field in the Content-Disposition header" 77 | )) 78 | } 79 | }; 80 | part.filename = mime.get_param("filename").map(|v| v.to_string()); 81 | }; 82 | if k == CONTENT_TYPE { 83 | part.mime = Some(v.to_str()?.parse()?) 84 | } 85 | headers_map.insert(k, v); 86 | } 87 | headers_map 88 | } 89 | Status::Partial => return Err(anyhow!("failed to parse field complete headers")), 90 | }; 91 | 92 | // Finding field data 93 | part.value = match buffer.read_to(format!("{}{}", constants::CRLF, boundary).as_bytes()) { 94 | Some(bytes) => bytes.to_vec(), 95 | None => return Err(anyhow!("incomplete multipart data, missing field data")), 96 | }; 97 | 98 | // Determine end of stream 99 | if buffer.read_until(boundary.as_bytes()).is_none() { 100 | return Err(anyhow!("incomplete multipart data, missing boundary")); 101 | }; 102 | let next_bytes = match buffer.peek_exact(constants::BOUNDARY_EXT.len()) { 103 | Some(bytes) => bytes, 104 | None => return Err(anyhow!("incomplete multipart data")), 105 | }; 106 | 107 | parts.insert(part.key.clone(), part); 108 | 109 | if next_bytes == constants::BOUNDARY_EXT.as_bytes() { 110 | return Ok(parts); 111 | } 112 | // discard \r\n. 113 | buffer.advance(constants::CRLF.len()); 114 | } 115 | } 116 | 117 | #[cfg(test)] 118 | mod tests { 119 | use super::*; 120 | 121 | #[test] 122 | fn test_parse() -> Result<()> { 123 | let data = b"--boundary\r\nContent-Disposition: form-data; name=field1\r\n\r\nvalue1\r\n--boundary\r\nContent-Disposition: form-data; name=field2; filename=file.txt\r\nContent-Type: text/plain\r\n\r\nhello\r\n--boundary--"; 124 | 125 | let parts = parse(data, "boundary")?; 126 | let field1 = parts.get("field1").unwrap(); 127 | assert_eq!(field1.key, "field1"); 128 | assert_eq!(field1.value, b"value1"); 129 | assert_eq!(field1.filename, None); 130 | assert_eq!(field1.mime, None); 131 | assert_eq!(field1.headers.len(), 1); 132 | 133 | let field2 = parts.get("field2").unwrap(); 134 | assert_eq!(field2.key, "field2"); 135 | assert_eq!(field2.value, b"hello"); 136 | assert_eq!(field2.filename, Some("file.txt".into())); 137 | assert_eq!(field2.mime, Some(mime::TEXT_PLAIN)); 138 | assert_eq!(field2.headers.len(), 2); 139 | Ok(()) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /waki/src/request.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | bindings::wasi::http::{ 3 | outgoing_handler, 4 | types::{IncomingRequest, OutgoingBody, OutgoingRequest, RequestOptions}, 5 | }, 6 | body::{write_to_outgoing_body, Body}, 7 | header::HeaderMap, 8 | ErrorCode, Method, Response, 9 | }; 10 | 11 | use anyhow::{anyhow, Error, Result}; 12 | use http::{ 13 | uri::{Authority, Parts, PathAndQuery}, 14 | Uri, 15 | }; 16 | use std::borrow::Borrow; 17 | use std::collections::HashMap; 18 | use std::time::Duration; 19 | 20 | pub struct RequestBuilder { 21 | // all errors generated while building the request will be deferred and returned when `send` the request. 22 | pub(crate) inner: Result, 23 | } 24 | 25 | impl RequestBuilder { 26 | #[inline] 27 | pub fn new(method: Method, uri: &str) -> Self { 28 | Self { 29 | inner: uri.parse::().map_or_else( 30 | |e| Err(Error::new(e)), 31 | |uri| Ok(Request::new(method, uri.into_parts())), 32 | ), 33 | } 34 | } 35 | 36 | /// Modify the query string of the Request URI. 37 | /// 38 | /// ``` 39 | /// # use anyhow::Result; 40 | /// # use waki::Client; 41 | /// # fn run() -> Result<()> { 42 | /// let resp = Client::new().get("https://httpbin.org/get") 43 | /// .query([("a", "b"), ("c", "d")]) 44 | /// .send()?; 45 | /// # Ok(()) 46 | /// # } 47 | /// ``` 48 | pub fn query(mut self, args: I) -> Self 49 | where 50 | K: AsRef, 51 | V: AsRef, 52 | I: IntoIterator, 53 | I::Item: Borrow<(K, V)>, 54 | { 55 | let mut err = None; 56 | if let Ok(ref mut req) = self.inner { 57 | let (path, query) = match &req.uri.path_and_query { 58 | Some(path_and_query) => ( 59 | path_and_query.path(), 60 | path_and_query.query().unwrap_or_default(), 61 | ), 62 | None => ("", ""), 63 | }; 64 | let mut serializer = form_urlencoded::Serializer::new(query.to_string()); 65 | serializer.extend_pairs(args); 66 | match PathAndQuery::try_from(format!("{}?{}", path, serializer.finish())) { 67 | Ok(path_and_query) => req.uri.path_and_query = Some(path_and_query), 68 | Err(e) => err = Some(e.into()), 69 | } 70 | } 71 | if let Some(e) = err { 72 | self.inner = Err(e); 73 | } 74 | self 75 | } 76 | 77 | /// Set the timeout for the initial connect to the HTTP Server. 78 | /// 79 | /// ``` 80 | /// # use anyhow::Result; 81 | /// # use std::time::Duration; 82 | /// # use waki::Client; 83 | /// # fn run() -> Result<()> { 84 | /// let resp = Client::new().post("https://httpbin.org/post") 85 | /// .connect_timeout(Duration::from_secs(5)) 86 | /// .send()?; 87 | /// # Ok(()) 88 | /// # } 89 | /// ``` 90 | #[inline] 91 | pub fn connect_timeout(mut self, timeout: Duration) -> Self { 92 | if let Ok(ref mut req) = self.inner { 93 | req.connect_timeout = Some(timeout.as_nanos() as u64); 94 | } 95 | self 96 | } 97 | 98 | /// Build the Request. 99 | #[inline] 100 | pub fn build(self) -> Result { 101 | self.inner 102 | } 103 | 104 | /// Send the Request, returning a [`Response`]. 105 | #[inline] 106 | pub fn send(self) -> Result { 107 | match self.inner { 108 | Ok(req) => req.send(), 109 | Err(e) => Err(e), 110 | } 111 | } 112 | } 113 | 114 | pub struct Request { 115 | method: Method, 116 | uri: Parts, 117 | pub(crate) headers: HeaderMap, 118 | pub(crate) body: Body, 119 | connect_timeout: Option, 120 | } 121 | 122 | impl TryFrom for Request { 123 | type Error = ErrorCode; 124 | 125 | fn try_from(req: IncomingRequest) -> std::result::Result { 126 | let method = req.method(); 127 | 128 | let mut parts = Parts::default(); 129 | if let Some(scheme) = req.scheme() { 130 | parts.scheme = Some( 131 | scheme 132 | .try_into() 133 | .map_err(|_| ErrorCode::HttpRequestUriInvalid)?, 134 | ); 135 | } 136 | if let Some(authority) = req.authority() { 137 | parts.authority = Some( 138 | authority 139 | .try_into() 140 | .map_err(|_| ErrorCode::HttpRequestUriInvalid)?, 141 | ); 142 | } 143 | if let Some(path_with_query) = req.path_with_query() { 144 | parts.path_and_query = Some( 145 | path_with_query 146 | .try_into() 147 | .map_err(|_| ErrorCode::HttpRequestUriInvalid)?, 148 | ); 149 | } 150 | 151 | let headers = req 152 | .headers_map() 153 | .map_err(|e| ErrorCode::InternalError(Some(e.to_string())))?; 154 | // The consume() method can only be called once 155 | let incoming_body = req.consume().unwrap(); 156 | drop(req); 157 | 158 | Ok(Self { 159 | method, 160 | uri: parts, 161 | headers, 162 | body: Body::Stream(incoming_body.into()), 163 | connect_timeout: None, 164 | }) 165 | } 166 | } 167 | 168 | impl Request { 169 | #[inline] 170 | pub fn new(method: Method, uri: Parts) -> Self { 171 | Self { 172 | method, 173 | uri, 174 | headers: HeaderMap::new(), 175 | body: Body::Bytes(vec![]), 176 | connect_timeout: None, 177 | } 178 | } 179 | 180 | #[inline] 181 | pub fn builder(method: Method, uri: &str) -> RequestBuilder { 182 | RequestBuilder::new(method, uri) 183 | } 184 | 185 | /// Get the HTTP method of the request. 186 | #[inline] 187 | pub fn method(&self) -> Method { 188 | self.method.clone() 189 | } 190 | 191 | /// Get the path of the request. 192 | #[inline] 193 | pub fn path(&self) -> &str { 194 | match &self.uri.path_and_query { 195 | Some(path_and_query) => path_and_query.path(), 196 | None => "", 197 | } 198 | } 199 | 200 | /// Get the query string of the request. 201 | pub fn query(&self) -> HashMap { 202 | match &self.uri.path_and_query { 203 | Some(path_and_query) => { 204 | let query_pairs = 205 | form_urlencoded::parse(path_and_query.query().unwrap_or_default().as_bytes()); 206 | query_pairs.into_owned().collect() 207 | } 208 | None => HashMap::default(), 209 | } 210 | } 211 | 212 | /// Get the authority of the request. 213 | #[inline] 214 | pub fn authority(&self) -> &Option { 215 | &self.uri.authority 216 | } 217 | 218 | fn send(self) -> Result { 219 | let req = OutgoingRequest::new(self.headers.try_into()?); 220 | req.set_method(&self.method) 221 | .map_err(|()| anyhow!("failed to set method"))?; 222 | if let Some(scheme) = self.uri.scheme { 223 | req.set_scheme(Some(&scheme.as_str().into())) 224 | .map_err(|()| anyhow!("failed to set scheme"))?; 225 | } 226 | if let Some(authority) = self.uri.authority { 227 | req.set_authority(Some(authority.as_str())) 228 | .map_err(|()| anyhow!("failed to set authority"))?; 229 | } 230 | if let Some(path_and_query) = self.uri.path_and_query { 231 | req.set_path_with_query(Some(path_and_query.as_str())) 232 | .map_err(|()| anyhow!("failed to set path_with_query"))?; 233 | } 234 | 235 | let outgoing_body = req 236 | .body() 237 | .map_err(|_| anyhow!("outgoing request write failed"))?; 238 | 239 | let options = RequestOptions::new(); 240 | options 241 | .set_connect_timeout(self.connect_timeout) 242 | .map_err(|()| anyhow!("failed to set connect_timeout"))?; 243 | let future_response = outgoing_handler::handle(req, Some(options))?; 244 | 245 | let body = self.body.bytes()?; 246 | write_to_outgoing_body(&outgoing_body, body.as_slice())?; 247 | OutgoingBody::finish(outgoing_body, None)?; 248 | 249 | let incoming_response = match future_response.get() { 250 | Some(result) => result.map_err(|()| anyhow!("response already taken"))?, 251 | None => { 252 | let pollable = future_response.subscribe(); 253 | pollable.block(); 254 | 255 | future_response 256 | .get() 257 | .expect("incoming response available") 258 | .map_err(|()| anyhow!("response already taken"))? 259 | } 260 | }?; 261 | drop(future_response); 262 | 263 | incoming_response.try_into() 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /waki/src/response.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | bindings::wasi::http::types::{ 3 | IncomingResponse, OutgoingBody, OutgoingResponse, ResponseOutparam, 4 | }, 5 | body::{write_to_outgoing_body, Body}, 6 | header::HeaderMap, 7 | ErrorCode, 8 | }; 9 | 10 | use anyhow::{Error, Result}; 11 | 12 | pub struct ResponseBuilder { 13 | // all errors generated while building the response will be deferred. 14 | pub(crate) inner: Result, 15 | } 16 | 17 | impl Default for ResponseBuilder { 18 | #[inline] 19 | fn default() -> Self { 20 | Self::new() 21 | } 22 | } 23 | 24 | impl ResponseBuilder { 25 | #[inline] 26 | pub fn new() -> Self { 27 | Self { 28 | inner: Ok(Response::new()), 29 | } 30 | } 31 | 32 | /// Set the status code for the response. 33 | /// 34 | /// Default value: 200. 35 | #[inline] 36 | pub fn status_code(mut self, status_code: u16) -> Self { 37 | if let Ok(ref mut resp) = self.inner { 38 | resp.status_code = status_code; 39 | } 40 | self 41 | } 42 | 43 | /// Build the Response. 44 | #[inline] 45 | pub fn build(self) -> Result { 46 | match self.inner { 47 | Ok(inner) => Ok(inner), 48 | Err(e) => Err(ErrorCode::InternalError(Some(e.to_string()))), 49 | } 50 | } 51 | } 52 | 53 | pub struct Response { 54 | pub(crate) headers: HeaderMap, 55 | pub(crate) body: Body, 56 | status_code: u16, 57 | } 58 | 59 | impl Default for Response { 60 | #[inline] 61 | fn default() -> Self { 62 | Self::new() 63 | } 64 | } 65 | 66 | impl TryFrom for Response { 67 | type Error = Error; 68 | 69 | fn try_from(incoming_response: IncomingResponse) -> std::result::Result { 70 | let status_code = incoming_response.status(); 71 | let headers = incoming_response.headers_map()?; 72 | // The consume() method can only be called once 73 | let incoming_body = incoming_response.consume().unwrap(); 74 | drop(incoming_response); 75 | 76 | Ok(Self { 77 | headers, 78 | status_code, 79 | body: Body::Stream(incoming_body.into()), 80 | }) 81 | } 82 | } 83 | 84 | impl Response { 85 | #[inline] 86 | pub fn new() -> Self { 87 | Self { 88 | headers: HeaderMap::new(), 89 | status_code: 200, 90 | body: Body::Bytes(vec![]), 91 | } 92 | } 93 | 94 | #[inline] 95 | pub fn builder() -> ResponseBuilder { 96 | ResponseBuilder::new() 97 | } 98 | 99 | #[inline] 100 | /// Get the status code of the response. 101 | pub fn status_code(&self) -> u16 { 102 | self.status_code 103 | } 104 | } 105 | 106 | pub fn handle_response(response_out: ResponseOutparam, response: Response) { 107 | let outgoing_response = OutgoingResponse::new(response.headers.try_into().unwrap()); 108 | outgoing_response 109 | .set_status_code(response.status_code) 110 | .unwrap(); 111 | let outgoing_body = outgoing_response.body().unwrap(); 112 | ResponseOutparam::set(response_out, Ok(outgoing_response)); 113 | 114 | let body = response.body.bytes().unwrap(); 115 | write_to_outgoing_body(&outgoing_body, body.as_slice()).unwrap(); 116 | OutgoingBody::finish(outgoing_body, None).unwrap(); 117 | } 118 | -------------------------------------------------------------------------------- /waki/tests/all/client.rs: -------------------------------------------------------------------------------- 1 | use super::run_wasi; 2 | 3 | #[tokio::test(flavor = "multi_thread")] 4 | async fn get_chunk() { 5 | run_wasi(test_programs_artifacts::CLIENT_GET_CHUNK_COMPONENT) 6 | .await 7 | .unwrap(); 8 | } 9 | 10 | #[tokio::test(flavor = "multi_thread")] 11 | async fn get_headers() { 12 | run_wasi(test_programs_artifacts::CLIENT_GET_HEADERS_COMPONENT) 13 | .await 14 | .unwrap(); 15 | } 16 | 17 | #[tokio::test(flavor = "multi_thread")] 18 | async fn get_with_query() { 19 | run_wasi(test_programs_artifacts::CLIENT_GET_WITH_QUERY_COMPONENT) 20 | .await 21 | .unwrap(); 22 | } 23 | 24 | #[tokio::test(flavor = "multi_thread")] 25 | async fn post_with_body() { 26 | run_wasi(test_programs_artifacts::CLIENT_POST_WITH_BODY_COMPONENT) 27 | .await 28 | .unwrap(); 29 | } 30 | 31 | #[tokio::test(flavor = "multi_thread")] 32 | async fn post_with_form_data() { 33 | run_wasi(test_programs_artifacts::CLIENT_POST_WITH_FORM_DATA_COMPONENT) 34 | .await 35 | .unwrap(); 36 | } 37 | 38 | #[tokio::test(flavor = "multi_thread")] 39 | async fn post_with_json_data() { 40 | run_wasi(test_programs_artifacts::CLIENT_POST_WITH_JSON_DATA_COMPONENT) 41 | .await 42 | .unwrap(); 43 | } 44 | 45 | #[tokio::test(flavor = "multi_thread")] 46 | async fn post_with_multipart_form_data() { 47 | run_wasi(test_programs_artifacts::CLIENT_POST_WITH_MULTIPART_FORM_DATA_COMPONENT) 48 | .await 49 | .unwrap(); 50 | } 51 | -------------------------------------------------------------------------------- /waki/tests/all/fixtures/file.txt: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /waki/tests/all/main.rs: -------------------------------------------------------------------------------- 1 | mod client; 2 | mod server; 3 | 4 | use anyhow::{Context, Result}; 5 | use http_body_util::{combinators::BoxBody, Collected}; 6 | use hyper::body::Bytes; 7 | use wasmtime::{ 8 | component::{Component, Linker, ResourceTable}, 9 | Config, Engine, Store, 10 | }; 11 | use wasmtime_wasi::{bindings::Command, DirPerms, FilePerms, WasiCtx, WasiCtxBuilder, WasiView}; 12 | use wasmtime_wasi_http::{ 13 | bindings::{ 14 | http::types::{ErrorCode, Scheme}, 15 | Proxy, 16 | }, 17 | WasiHttpCtx, WasiHttpView, 18 | }; 19 | 20 | struct Ctx { 21 | table: ResourceTable, 22 | wasi: WasiCtx, 23 | http: WasiHttpCtx, 24 | } 25 | 26 | impl WasiView for Ctx { 27 | fn table(&mut self) -> &mut ResourceTable { 28 | &mut self.table 29 | } 30 | fn ctx(&mut self) -> &mut WasiCtx { 31 | &mut self.wasi 32 | } 33 | } 34 | 35 | impl WasiHttpView for Ctx { 36 | fn ctx(&mut self) -> &mut WasiHttpCtx { 37 | &mut self.http 38 | } 39 | 40 | fn table(&mut self) -> &mut ResourceTable { 41 | &mut self.table 42 | } 43 | } 44 | 45 | fn new_component(component_filename: &str) -> Result<(Store, Component, Linker)> { 46 | let mut config = Config::new(); 47 | config.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable); 48 | config.wasm_component_model(true); 49 | config.async_support(true); 50 | 51 | let engine = Engine::new(&config)?; 52 | let component = Component::from_file(&engine, component_filename)?; 53 | 54 | let ctx = Ctx { 55 | table: ResourceTable::new(), 56 | wasi: WasiCtxBuilder::new() 57 | .inherit_stdio() 58 | .preopened_dir("./tests/all/fixtures", ".", DirPerms::READ, FilePerms::READ)? 59 | .build(), 60 | http: WasiHttpCtx::new(), 61 | }; 62 | 63 | let store = Store::new(&engine, ctx); 64 | let mut linker = Linker::new(&engine); 65 | wasmtime_wasi::add_to_linker_async(&mut linker)?; 66 | wasmtime_wasi_http::add_only_http_to_linker_async(&mut linker)?; 67 | Ok((store, component, linker)) 68 | } 69 | 70 | // ref: https://github.com/bytecodealliance/wasmtime/blob/af59c4d568d487b7efbb49d7d31a861e7c3933a6/crates/wasi-http/tests/all/main.rs#L129 71 | pub async fn run_wasi_http( 72 | component_filename: &str, 73 | req: hyper::Request>, 74 | ) -> Result>, ErrorCode>> { 75 | let (mut store, component, linker) = new_component(component_filename)?; 76 | 77 | let proxy = Proxy::instantiate_async(&mut store, &component, &linker).await?; 78 | 79 | let req = store.data_mut().new_incoming_request(Scheme::Http, req)?; 80 | 81 | let (sender, receiver) = tokio::sync::oneshot::channel(); 82 | let out = store.data_mut().new_response_outparam(sender)?; 83 | 84 | let handle = wasmtime_wasi::runtime::spawn(async move { 85 | proxy 86 | .wasi_http_incoming_handler() 87 | .call_handle(&mut store, req, out) 88 | .await?; 89 | 90 | Ok::<_, anyhow::Error>(()) 91 | }); 92 | 93 | let resp = match receiver.await { 94 | Ok(Ok(resp)) => { 95 | use http_body_util::BodyExt; 96 | let (parts, body) = resp.into_parts(); 97 | let collected = BodyExt::collect(body).await?; 98 | Some(Ok(hyper::Response::from_parts(parts, collected))) 99 | } 100 | Ok(Err(e)) => Some(Err(e)), 101 | 102 | // Fall through below to the `resp.expect(...)` which will hopefully 103 | // return a more specific error from `handle.await`. 104 | Err(_) => None, 105 | }; 106 | 107 | // Now that the response has been processed, we can wait on the wasm to 108 | // finish without deadlocking. 109 | handle.await.context("Component execution")?; 110 | 111 | Ok(resp.expect("wasm never called set-response-outparam")) 112 | } 113 | 114 | pub async fn run_wasi(component_filename: &str) -> Result<()> { 115 | let (mut store, component, linker) = new_component(component_filename)?; 116 | 117 | let command = Command::instantiate_async(&mut store, &component, &linker).await?; 118 | command 119 | .wasi_cli_run() 120 | .call_run(&mut store) 121 | .await? 122 | .map_err(|()| anyhow::anyhow!("run returned a failure")) 123 | } 124 | -------------------------------------------------------------------------------- /waki/tests/all/server.rs: -------------------------------------------------------------------------------- 1 | use super::run_wasi_http; 2 | 3 | use anyhow::Result; 4 | 5 | #[tokio::test(flavor = "multi_thread")] 6 | async fn form() -> Result<()> { 7 | let req = hyper::Request::builder() 8 | .uri("http://localhost") 9 | .body(body::full("key1=Hello&key2=World"))?; 10 | 11 | let resp = run_wasi_http(test_programs_artifacts::SERVER_FORM_COMPONENT, req).await??; 12 | let body = resp.into_body().to_bytes(); 13 | let body = std::str::from_utf8(&body)?; 14 | assert_eq!(body, "Hello World"); 15 | 16 | Ok(()) 17 | } 18 | 19 | #[tokio::test(flavor = "multi_thread")] 20 | async fn json() -> Result<()> { 21 | let req = hyper::Request::builder() 22 | .uri("http://localhost") 23 | .body(body::full("{\"data\": \"Hello World\"}"))?; 24 | 25 | let resp = run_wasi_http(test_programs_artifacts::SERVER_JSON_COMPONENT, req).await??; 26 | let body = resp.into_body().to_bytes(); 27 | let body = std::str::from_utf8(&body)?; 28 | assert_eq!(body, "Hello World"); 29 | 30 | Ok(()) 31 | } 32 | 33 | #[tokio::test(flavor = "multi_thread")] 34 | async fn large_body() -> Result<()> { 35 | let req = hyper::Request::builder() 36 | .uri("http://localhost") 37 | .body(body::empty())?; 38 | 39 | let resp = run_wasi_http(test_programs_artifacts::SERVER_LARGE_BODY_COMPONENT, req).await??; 40 | let body = resp.into_body().to_bytes(); 41 | assert_eq!(body.len(), 5000); 42 | 43 | Ok(()) 44 | } 45 | 46 | #[tokio::test(flavor = "multi_thread")] 47 | async fn multipart_form() -> Result<()> { 48 | let req = hyper::Request::builder() 49 | .uri("http://localhost") 50 | .header("Content-Type", "multipart/form-data; boundary=boundary") 51 | .body(body::full("--boundary\r\nContent-Disposition: form-data; name=form\r\n\r\nHello \r\n--boundary\r\nContent-Disposition: form-data; name=file; filename=file.txt\r\nContent-Type: text/plain\r\n\r\nWorld\r\n--boundary--"))?; 52 | 53 | let resp = run_wasi_http( 54 | test_programs_artifacts::SERVER_MULTIPART_FORM_COMPONENT, 55 | req, 56 | ) 57 | .await??; 58 | let body = resp.into_body().to_bytes(); 59 | let body = std::str::from_utf8(&body)?; 60 | assert_eq!(body, "Hello World"); 61 | 62 | Ok(()) 63 | } 64 | 65 | #[tokio::test(flavor = "multi_thread")] 66 | async fn query() -> Result<()> { 67 | let req = hyper::Request::builder() 68 | .uri("http://localhost?name=ia") 69 | .body(body::empty())?; 70 | 71 | let resp = run_wasi_http(test_programs_artifacts::SERVER_QUERY_COMPONENT, req).await??; 72 | let body = resp.into_body().to_bytes(); 73 | let body = std::str::from_utf8(&body)?; 74 | assert_eq!(body, "Hello, ia!"); 75 | 76 | Ok(()) 77 | } 78 | 79 | #[tokio::test(flavor = "multi_thread")] 80 | async fn status_code() -> Result<()> { 81 | let req = hyper::Request::builder() 82 | .uri("http://localhost") 83 | .body(body::empty())?; 84 | 85 | let resp = run_wasi_http(test_programs_artifacts::SERVER_STATUS_CODE_COMPONENT, req).await??; 86 | assert_eq!(resp.status(), 400); 87 | 88 | Ok(()) 89 | } 90 | 91 | #[tokio::test(flavor = "multi_thread")] 92 | async fn authority() -> Result<()> { 93 | let req = hyper::Request::builder() 94 | .uri("http://127.0.0.1:3000") 95 | .body(body::empty())?; 96 | 97 | let resp: http::Response> = 98 | run_wasi_http(test_programs_artifacts::SERVER_AUTHORITY_COMPONENT, req).await??; 99 | let body = resp.into_body().to_bytes(); 100 | let body = std::str::from_utf8(&body)?; 101 | assert_eq!(body, "Hello, 127.0.0.1:3000!"); 102 | 103 | Ok(()) 104 | } 105 | 106 | mod body { 107 | use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full}; 108 | use hyper::{body::Bytes, Error}; 109 | 110 | pub fn full(bytes: &'static str) -> BoxBody { 111 | BoxBody::new(Full::new(bytes.into()).map_err(|_| unreachable!())) 112 | } 113 | 114 | pub fn empty() -> BoxBody { 115 | BoxBody::new(Empty::new().map_err(|_| unreachable!())) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /waki/wit/deps/cli/command.wit: -------------------------------------------------------------------------------- 1 | package wasi:cli@0.2.2; 2 | 3 | @since(version = 0.2.0) 4 | world command { 5 | @since(version = 0.2.0) 6 | include imports; 7 | 8 | @since(version = 0.2.0) 9 | export run; 10 | } 11 | -------------------------------------------------------------------------------- /waki/wit/deps/cli/environment.wit: -------------------------------------------------------------------------------- 1 | @since(version = 0.2.0) 2 | interface environment { 3 | /// Get the POSIX-style environment variables. 4 | /// 5 | /// Each environment variable is provided as a pair of string variable names 6 | /// and string value. 7 | /// 8 | /// Morally, these are a value import, but until value imports are available 9 | /// in the component model, this import function should return the same 10 | /// values each time it is called. 11 | @since(version = 0.2.0) 12 | get-environment: func() -> list>; 13 | 14 | /// Get the POSIX-style arguments to the program. 15 | @since(version = 0.2.0) 16 | get-arguments: func() -> list; 17 | 18 | /// Return a path that programs should use as their initial current working 19 | /// directory, interpreting `.` as shorthand for this. 20 | @since(version = 0.2.0) 21 | initial-cwd: func() -> option; 22 | } 23 | -------------------------------------------------------------------------------- /waki/wit/deps/cli/exit.wit: -------------------------------------------------------------------------------- 1 | @since(version = 0.2.0) 2 | interface exit { 3 | /// Exit the current instance and any linked instances. 4 | @since(version = 0.2.0) 5 | exit: func(status: result); 6 | 7 | /// Exit the current instance and any linked instances, reporting the 8 | /// specified status code to the host. 9 | /// 10 | /// The meaning of the code depends on the context, with 0 usually meaning 11 | /// "success", and other values indicating various types of failure. 12 | /// 13 | /// This function does not return; the effect is analogous to a trap, but 14 | /// without the connotation that something bad has happened. 15 | @unstable(feature = cli-exit-with-code) 16 | exit-with-code: func(status-code: u8); 17 | } 18 | -------------------------------------------------------------------------------- /waki/wit/deps/cli/imports.wit: -------------------------------------------------------------------------------- 1 | package wasi:cli@0.2.2; 2 | 3 | @since(version = 0.2.0) 4 | world imports { 5 | @since(version = 0.2.0) 6 | include wasi:clocks/imports@0.2.2; 7 | @since(version = 0.2.0) 8 | include wasi:filesystem/imports@0.2.2; 9 | @since(version = 0.2.0) 10 | include wasi:sockets/imports@0.2.2; 11 | @since(version = 0.2.0) 12 | include wasi:random/imports@0.2.2; 13 | @since(version = 0.2.0) 14 | include wasi:io/imports@0.2.2; 15 | 16 | @since(version = 0.2.0) 17 | import environment; 18 | @since(version = 0.2.0) 19 | import exit; 20 | @since(version = 0.2.0) 21 | import stdin; 22 | @since(version = 0.2.0) 23 | import stdout; 24 | @since(version = 0.2.0) 25 | import stderr; 26 | @since(version = 0.2.0) 27 | import terminal-input; 28 | @since(version = 0.2.0) 29 | import terminal-output; 30 | @since(version = 0.2.0) 31 | import terminal-stdin; 32 | @since(version = 0.2.0) 33 | import terminal-stdout; 34 | @since(version = 0.2.0) 35 | import terminal-stderr; 36 | } 37 | -------------------------------------------------------------------------------- /waki/wit/deps/cli/run.wit: -------------------------------------------------------------------------------- 1 | @since(version = 0.2.0) 2 | interface run { 3 | /// Run the program. 4 | @since(version = 0.2.0) 5 | run: func() -> result; 6 | } 7 | -------------------------------------------------------------------------------- /waki/wit/deps/cli/stdio.wit: -------------------------------------------------------------------------------- 1 | @since(version = 0.2.0) 2 | interface stdin { 3 | @since(version = 0.2.0) 4 | use wasi:io/streams@0.2.2.{input-stream}; 5 | 6 | @since(version = 0.2.0) 7 | get-stdin: func() -> input-stream; 8 | } 9 | 10 | @since(version = 0.2.0) 11 | interface stdout { 12 | @since(version = 0.2.0) 13 | use wasi:io/streams@0.2.2.{output-stream}; 14 | 15 | @since(version = 0.2.0) 16 | get-stdout: func() -> output-stream; 17 | } 18 | 19 | @since(version = 0.2.0) 20 | interface stderr { 21 | @since(version = 0.2.0) 22 | use wasi:io/streams@0.2.2.{output-stream}; 23 | 24 | @since(version = 0.2.0) 25 | get-stderr: func() -> output-stream; 26 | } 27 | -------------------------------------------------------------------------------- /waki/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 | @since(version = 0.2.0) 7 | interface terminal-input { 8 | /// The input side of a terminal. 9 | @since(version = 0.2.0) 10 | resource terminal-input; 11 | } 12 | 13 | /// Terminal output. 14 | /// 15 | /// In the future, this may include functions for querying the terminal 16 | /// size, being notified of terminal size changes, querying supported 17 | /// features, and so on. 18 | @since(version = 0.2.0) 19 | interface terminal-output { 20 | /// The output side of a terminal. 21 | @since(version = 0.2.0) 22 | resource terminal-output; 23 | } 24 | 25 | /// An interface providing an optional `terminal-input` for stdin as a 26 | /// link-time authority. 27 | @since(version = 0.2.0) 28 | interface terminal-stdin { 29 | @since(version = 0.2.0) 30 | use terminal-input.{terminal-input}; 31 | 32 | /// If stdin is connected to a terminal, return a `terminal-input` handle 33 | /// allowing further interaction with it. 34 | @since(version = 0.2.0) 35 | get-terminal-stdin: func() -> option; 36 | } 37 | 38 | /// An interface providing an optional `terminal-output` for stdout as a 39 | /// link-time authority. 40 | @since(version = 0.2.0) 41 | interface terminal-stdout { 42 | @since(version = 0.2.0) 43 | use terminal-output.{terminal-output}; 44 | 45 | /// If stdout is connected to a terminal, return a `terminal-output` handle 46 | /// allowing further interaction with it. 47 | @since(version = 0.2.0) 48 | get-terminal-stdout: func() -> option; 49 | } 50 | 51 | /// An interface providing an optional `terminal-output` for stderr as a 52 | /// link-time authority. 53 | @since(version = 0.2.0) 54 | interface terminal-stderr { 55 | @since(version = 0.2.0) 56 | use terminal-output.{terminal-output}; 57 | 58 | /// If stderr is connected to a terminal, return a `terminal-output` handle 59 | /// allowing further interaction with it. 60 | @since(version = 0.2.0) 61 | get-terminal-stderr: func() -> option; 62 | } 63 | -------------------------------------------------------------------------------- /waki/wit/deps/clocks/monotonic-clock.wit: -------------------------------------------------------------------------------- 1 | package wasi:clocks@0.2.2; 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 | @since(version = 0.2.0) 11 | interface monotonic-clock { 12 | @since(version = 0.2.0) 13 | use wasi:io/poll@0.2.2.{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 | @since(version = 0.2.0) 19 | type instant = u64; 20 | 21 | /// A duration of time, in nanoseconds. 22 | @since(version = 0.2.0) 23 | type duration = u64; 24 | 25 | /// Read the current value of the clock. 26 | /// 27 | /// The clock is monotonic, therefore calling this function repeatedly will 28 | /// produce a sequence of non-decreasing values. 29 | @since(version = 0.2.0) 30 | now: func() -> instant; 31 | 32 | /// Query the resolution of the clock. Returns the duration of time 33 | /// corresponding to a clock tick. 34 | @since(version = 0.2.0) 35 | resolution: func() -> duration; 36 | 37 | /// Create a `pollable` which will resolve once the specified instant 38 | /// has occurred. 39 | @since(version = 0.2.0) 40 | subscribe-instant: func( 41 | when: instant, 42 | ) -> pollable; 43 | 44 | /// Create a `pollable` that will resolve after the specified duration has 45 | /// elapsed from the time this function is invoked. 46 | @since(version = 0.2.0) 47 | subscribe-duration: func( 48 | when: duration, 49 | ) -> pollable; 50 | } 51 | -------------------------------------------------------------------------------- /waki/wit/deps/clocks/timezone.wit: -------------------------------------------------------------------------------- 1 | package wasi:clocks@0.2.2; 2 | 3 | @unstable(feature = clocks-timezone) 4 | interface timezone { 5 | @unstable(feature = clocks-timezone) 6 | use wall-clock.{datetime}; 7 | 8 | /// Return information needed to display the given `datetime`. This includes 9 | /// the UTC offset, the time zone name, and a flag indicating whether 10 | /// daylight saving time is active. 11 | /// 12 | /// If the timezone cannot be determined for the given `datetime`, return a 13 | /// `timezone-display` for `UTC` with a `utc-offset` of 0 and no daylight 14 | /// saving time. 15 | @unstable(feature = clocks-timezone) 16 | display: func(when: datetime) -> timezone-display; 17 | 18 | /// The same as `display`, but only return the UTC offset. 19 | @unstable(feature = clocks-timezone) 20 | utc-offset: func(when: datetime) -> s32; 21 | 22 | /// Information useful for displaying the timezone of a specific `datetime`. 23 | /// 24 | /// This information may vary within a single `timezone` to reflect daylight 25 | /// saving time adjustments. 26 | @unstable(feature = clocks-timezone) 27 | record timezone-display { 28 | /// The number of seconds difference between UTC time and the local 29 | /// time of the timezone. 30 | /// 31 | /// The returned value will always be less than 86400 which is the 32 | /// number of seconds in a day (24*60*60). 33 | /// 34 | /// In implementations that do not expose an actual time zone, this 35 | /// should return 0. 36 | utc-offset: s32, 37 | 38 | /// The abbreviated name of the timezone to display to a user. The name 39 | /// `UTC` indicates Coordinated Universal Time. Otherwise, this should 40 | /// reference local standards for the name of the time zone. 41 | /// 42 | /// In implementations that do not expose an actual time zone, this 43 | /// should be the string `UTC`. 44 | /// 45 | /// In time zones that do not have an applicable name, a formatted 46 | /// representation of the UTC offset may be returned, such as `-04:00`. 47 | name: string, 48 | 49 | /// Whether daylight saving time is active. 50 | /// 51 | /// In implementations that do not expose an actual time zone, this 52 | /// should return false. 53 | in-daylight-saving-time: bool, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /waki/wit/deps/clocks/wall-clock.wit: -------------------------------------------------------------------------------- 1 | package wasi:clocks@0.2.2; 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 | @since(version = 0.2.0) 17 | interface wall-clock { 18 | /// A time and date in seconds plus nanoseconds. 19 | @since(version = 0.2.0) 20 | record datetime { 21 | seconds: u64, 22 | nanoseconds: u32, 23 | } 24 | 25 | /// Read the current value of the clock. 26 | /// 27 | /// This clock is not monotonic, therefore calling this function repeatedly 28 | /// will not necessarily produce a sequence of non-decreasing values. 29 | /// 30 | /// The returned timestamps represent the number of seconds since 31 | /// 1970-01-01T00:00:00Z, also known as [POSIX's Seconds Since the Epoch], 32 | /// also known as [Unix Time]. 33 | /// 34 | /// The nanoseconds field of the output is always less than 1000000000. 35 | /// 36 | /// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16 37 | /// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time 38 | @since(version = 0.2.0) 39 | now: func() -> datetime; 40 | 41 | /// Query the resolution of the clock. 42 | /// 43 | /// The nanoseconds field of the output is always less than 1000000000. 44 | @since(version = 0.2.0) 45 | resolution: func() -> datetime; 46 | } 47 | -------------------------------------------------------------------------------- /waki/wit/deps/clocks/world.wit: -------------------------------------------------------------------------------- 1 | package wasi:clocks@0.2.2; 2 | 3 | @since(version = 0.2.0) 4 | world imports { 5 | @since(version = 0.2.0) 6 | import monotonic-clock; 7 | @since(version = 0.2.0) 8 | import wall-clock; 9 | @unstable(feature = clocks-timezone) 10 | import timezone; 11 | } 12 | -------------------------------------------------------------------------------- /waki/wit/deps/filesystem/preopens.wit: -------------------------------------------------------------------------------- 1 | package wasi:filesystem@0.2.2; 2 | 3 | @since(version = 0.2.0) 4 | interface preopens { 5 | @since(version = 0.2.0) 6 | use types.{descriptor}; 7 | 8 | /// Return the set of preopened directories, and their path. 9 | @since(version = 0.2.0) 10 | get-directories: func() -> list>; 11 | } 12 | -------------------------------------------------------------------------------- /waki/wit/deps/filesystem/world.wit: -------------------------------------------------------------------------------- 1 | package wasi:filesystem@0.2.2; 2 | 3 | @since(version = 0.2.0) 4 | world imports { 5 | @since(version = 0.2.0) 6 | import types; 7 | @since(version = 0.2.0) 8 | import preopens; 9 | } 10 | -------------------------------------------------------------------------------- /waki/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 | @since(version = 0.2.0) 4 | interface incoming-handler { 5 | @since(version = 0.2.0) 6 | use types.{incoming-request, response-outparam}; 7 | 8 | /// This function is invoked with an incoming HTTP Request, and a resource 9 | /// `response-outparam` which provides the capability to reply with an HTTP 10 | /// Response. The response is sent by calling the `response-outparam.set` 11 | /// method, which allows execution to continue after the response has been 12 | /// sent. This enables both streaming to the response body, and performing other 13 | /// work. 14 | /// 15 | /// The implementor of this function must write a response to the 16 | /// `response-outparam` before returning, or else the caller will respond 17 | /// with an error on its behalf. 18 | @since(version = 0.2.0) 19 | handle: func( 20 | request: incoming-request, 21 | response-out: response-outparam 22 | ); 23 | } 24 | 25 | /// This interface defines a handler of outgoing HTTP Requests. It should be 26 | /// imported by components which wish to make HTTP Requests. 27 | @since(version = 0.2.0) 28 | interface outgoing-handler { 29 | @since(version = 0.2.0) 30 | use types.{ 31 | outgoing-request, request-options, future-incoming-response, error-code 32 | }; 33 | 34 | /// This function is invoked with an outgoing HTTP Request, and it returns 35 | /// a resource `future-incoming-response` which represents an HTTP Response 36 | /// which may arrive in the future. 37 | /// 38 | /// The `options` argument accepts optional parameters for the HTTP 39 | /// protocol's transport layer. 40 | /// 41 | /// This function may return an error if the `outgoing-request` is invalid 42 | /// or not allowed to be made. Otherwise, protocol errors are reported 43 | /// through the `future-incoming-response`. 44 | @since(version = 0.2.0) 45 | handle: func( 46 | request: outgoing-request, 47 | options: option 48 | ) -> result; 49 | } 50 | -------------------------------------------------------------------------------- /waki/wit/deps/http/proxy.wit: -------------------------------------------------------------------------------- 1 | package wasi:http@0.2.2; 2 | 3 | /// The `wasi:http/imports` world imports all the APIs for HTTP proxies. 4 | /// It is intended to be `include`d in other worlds. 5 | @since(version = 0.2.0) 6 | world imports { 7 | /// HTTP proxies have access to time and randomness. 8 | @since(version = 0.2.0) 9 | import wasi:clocks/monotonic-clock@0.2.2; 10 | @since(version = 0.2.0) 11 | import wasi:clocks/wall-clock@0.2.2; 12 | @since(version = 0.2.0) 13 | import wasi:random/random@0.2.2; 14 | 15 | /// Proxies have standard output and error streams which are expected to 16 | /// terminate in a developer-facing console provided by the host. 17 | @since(version = 0.2.0) 18 | import wasi:cli/stdout@0.2.2; 19 | @since(version = 0.2.0) 20 | import wasi:cli/stderr@0.2.2; 21 | 22 | /// TODO: this is a temporary workaround until component tooling is able to 23 | /// gracefully handle the absence of stdin. Hosts must return an eof stream 24 | /// for this import, which is what wasi-libc + tooling will do automatically 25 | /// when this import is properly removed. 26 | @since(version = 0.2.0) 27 | import wasi:cli/stdin@0.2.2; 28 | 29 | /// This is the default handler to use when user code simply wants to make an 30 | /// HTTP request (e.g., via `fetch()`). 31 | @since(version = 0.2.0) 32 | import outgoing-handler; 33 | } 34 | 35 | /// The `wasi:http/proxy` world captures a widely-implementable intersection of 36 | /// hosts that includes HTTP forward and reverse proxies. Components targeting 37 | /// this world may concurrently stream in and out any number of incoming and 38 | /// outgoing HTTP requests. 39 | @since(version = 0.2.0) 40 | world proxy { 41 | @since(version = 0.2.0) 42 | include imports; 43 | 44 | /// The host delivers incoming HTTP requests to a component by calling the 45 | /// `handle` function of this exported interface. A host may arbitrarily reuse 46 | /// or not reuse component instance when delivering incoming HTTP requests and 47 | /// thus a component must be able to handle 0..N calls to `handle`. 48 | @since(version = 0.2.0) 49 | export incoming-handler; 50 | } 51 | -------------------------------------------------------------------------------- /waki/wit/deps/http/types.wit: -------------------------------------------------------------------------------- 1 | /// This interface defines all of the types and methods for implementing 2 | /// HTTP Requests and Responses, both incoming and outgoing, as well as 3 | /// their headers, trailers, and bodies. 4 | @since(version = 0.2.0) 5 | interface types { 6 | @since(version = 0.2.0) 7 | use wasi:clocks/monotonic-clock@0.2.2.{duration}; 8 | @since(version = 0.2.0) 9 | use wasi:io/streams@0.2.2.{input-stream, output-stream}; 10 | @since(version = 0.2.0) 11 | use wasi:io/error@0.2.2.{error as io-error}; 12 | @since(version = 0.2.0) 13 | use wasi:io/poll@0.2.2.{pollable}; 14 | 15 | /// This type corresponds to HTTP standard Methods. 16 | @since(version = 0.2.0) 17 | variant method { 18 | get, 19 | head, 20 | post, 21 | put, 22 | delete, 23 | connect, 24 | options, 25 | trace, 26 | patch, 27 | other(string) 28 | } 29 | 30 | /// This type corresponds to HTTP standard Related Schemes. 31 | @since(version = 0.2.0) 32 | variant scheme { 33 | HTTP, 34 | HTTPS, 35 | other(string) 36 | } 37 | 38 | /// These cases are inspired by the IANA HTTP Proxy Error Types: 39 | /// https://www.iana.org/assignments/http-proxy-status/http-proxy-status.xhtml#table-http-proxy-error-types 40 | @since(version = 0.2.0) 41 | variant error-code { 42 | DNS-timeout, 43 | DNS-error(DNS-error-payload), 44 | destination-not-found, 45 | destination-unavailable, 46 | destination-IP-prohibited, 47 | destination-IP-unroutable, 48 | connection-refused, 49 | connection-terminated, 50 | connection-timeout, 51 | connection-read-timeout, 52 | connection-write-timeout, 53 | connection-limit-reached, 54 | TLS-protocol-error, 55 | TLS-certificate-error, 56 | TLS-alert-received(TLS-alert-received-payload), 57 | HTTP-request-denied, 58 | HTTP-request-length-required, 59 | HTTP-request-body-size(option), 60 | HTTP-request-method-invalid, 61 | HTTP-request-URI-invalid, 62 | HTTP-request-URI-too-long, 63 | HTTP-request-header-section-size(option), 64 | HTTP-request-header-size(option), 65 | HTTP-request-trailer-section-size(option), 66 | HTTP-request-trailer-size(field-size-payload), 67 | HTTP-response-incomplete, 68 | HTTP-response-header-section-size(option), 69 | HTTP-response-header-size(field-size-payload), 70 | HTTP-response-body-size(option), 71 | HTTP-response-trailer-section-size(option), 72 | HTTP-response-trailer-size(field-size-payload), 73 | HTTP-response-transfer-coding(option), 74 | HTTP-response-content-coding(option), 75 | HTTP-response-timeout, 76 | HTTP-upgrade-failed, 77 | HTTP-protocol-error, 78 | loop-detected, 79 | configuration-error, 80 | /// This is a catch-all error for anything that doesn't fit cleanly into a 81 | /// more specific case. It also includes an optional string for an 82 | /// unstructured description of the error. Users should not depend on the 83 | /// string for diagnosing errors, as it's not required to be consistent 84 | /// between implementations. 85 | internal-error(option) 86 | } 87 | 88 | /// Defines the case payload type for `DNS-error` above: 89 | @since(version = 0.2.0) 90 | record DNS-error-payload { 91 | rcode: option, 92 | info-code: option 93 | } 94 | 95 | /// Defines the case payload type for `TLS-alert-received` above: 96 | @since(version = 0.2.0) 97 | record TLS-alert-received-payload { 98 | alert-id: option, 99 | alert-message: option 100 | } 101 | 102 | /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above: 103 | @since(version = 0.2.0) 104 | record field-size-payload { 105 | field-name: option, 106 | field-size: option 107 | } 108 | 109 | /// Attempts to extract a http-related `error` from the wasi:io `error` 110 | /// provided. 111 | /// 112 | /// Stream operations which return 113 | /// `wasi:io/stream/stream-error::last-operation-failed` have a payload of 114 | /// type `wasi:io/error/error` with more information about the operation 115 | /// that failed. This payload can be passed through to this function to see 116 | /// if there's http-related information about the error to return. 117 | /// 118 | /// Note that this function is fallible because not all io-errors are 119 | /// http-related errors. 120 | @since(version = 0.2.0) 121 | http-error-code: func(err: borrow) -> option; 122 | 123 | /// This type enumerates the different kinds of errors that may occur when 124 | /// setting or appending to a `fields` resource. 125 | @since(version = 0.2.0) 126 | variant header-error { 127 | /// This error indicates that a `field-name` or `field-value` was 128 | /// syntactically invalid when used with an operation that sets headers in a 129 | /// `fields`. 130 | invalid-syntax, 131 | 132 | /// This error indicates that a forbidden `field-name` was used when trying 133 | /// to set a header in a `fields`. 134 | forbidden, 135 | 136 | /// This error indicates that the operation on the `fields` was not 137 | /// permitted because the fields are immutable. 138 | immutable, 139 | } 140 | 141 | /// Field names are always strings. 142 | /// 143 | /// Field names should always be treated as case insensitive by the `fields` 144 | /// resource for the purposes of equality checking. 145 | @since(version = 0.2.1) 146 | type field-name = field-key; 147 | 148 | /// Field keys are always strings. 149 | /// 150 | /// Field keys should always be treated as case insensitive by the `fields` 151 | /// resource for the purposes of equality checking. 152 | /// 153 | /// # Deprecation 154 | /// 155 | /// This type has been deprecated in favor of the `field-name` type. 156 | @since(version = 0.2.0) 157 | @deprecated(version = 0.2.2) 158 | type field-key = string; 159 | 160 | /// Field values should always be ASCII strings. However, in 161 | /// reality, HTTP implementations often have to interpret malformed values, 162 | /// so they are provided as a list of bytes. 163 | @since(version = 0.2.0) 164 | type field-value = list; 165 | 166 | /// This following block defines the `fields` resource which corresponds to 167 | /// HTTP standard Fields. Fields are a common representation used for both 168 | /// Headers and Trailers. 169 | /// 170 | /// A `fields` may be mutable or immutable. A `fields` created using the 171 | /// constructor, `from-list`, or `clone` will be mutable, but a `fields` 172 | /// resource given by other means (including, but not limited to, 173 | /// `incoming-request.headers`, `outgoing-request.headers`) might be be 174 | /// immutable. In an immutable fields, the `set`, `append`, and `delete` 175 | /// operations will fail with `header-error.immutable`. 176 | @since(version = 0.2.0) 177 | resource fields { 178 | 179 | /// Construct an empty HTTP Fields. 180 | /// 181 | /// The resulting `fields` is mutable. 182 | @since(version = 0.2.0) 183 | constructor(); 184 | 185 | /// Construct an HTTP Fields. 186 | /// 187 | /// The resulting `fields` is mutable. 188 | /// 189 | /// The list represents each name-value pair in the Fields. Names 190 | /// which have multiple values are represented by multiple entries in this 191 | /// list with the same name. 192 | /// 193 | /// The tuple is a pair of the field name, represented as a string, and 194 | /// Value, represented as a list of bytes. 195 | /// 196 | /// An error result will be returned if any `field-name` or `field-value` is 197 | /// syntactically invalid, or if a field is forbidden. 198 | @since(version = 0.2.0) 199 | from-list: static func( 200 | entries: list> 201 | ) -> result; 202 | 203 | /// Get all of the values corresponding to a name. If the name is not present 204 | /// in this `fields` or is syntactically invalid, an empty list is returned. 205 | /// However, if the name is present but empty, this is represented by a list 206 | /// with one or more empty field-values present. 207 | @since(version = 0.2.0) 208 | get: func(name: field-name) -> list; 209 | 210 | /// Returns `true` when the name is present in this `fields`. If the name is 211 | /// syntactically invalid, `false` is returned. 212 | @since(version = 0.2.0) 213 | has: func(name: field-name) -> bool; 214 | 215 | /// Set all of the values for a name. Clears any existing values for that 216 | /// name, if they have been set. 217 | /// 218 | /// Fails with `header-error.immutable` if the `fields` are immutable. 219 | /// 220 | /// Fails with `header-error.invalid-syntax` if the `field-name` or any of 221 | /// the `field-value`s are syntactically invalid. 222 | @since(version = 0.2.0) 223 | set: func(name: field-name, value: list) -> result<_, header-error>; 224 | 225 | /// Delete all values for a name. Does nothing if no values for the name 226 | /// exist. 227 | /// 228 | /// Fails with `header-error.immutable` if the `fields` are immutable. 229 | /// 230 | /// Fails with `header-error.invalid-syntax` if the `field-name` is 231 | /// syntactically invalid. 232 | @since(version = 0.2.0) 233 | delete: func(name: field-name) -> result<_, header-error>; 234 | 235 | /// Append a value for a name. Does not change or delete any existing 236 | /// values for that name. 237 | /// 238 | /// Fails with `header-error.immutable` if the `fields` are immutable. 239 | /// 240 | /// Fails with `header-error.invalid-syntax` if the `field-name` or 241 | /// `field-value` are syntactically invalid. 242 | @since(version = 0.2.0) 243 | append: func(name: field-name, value: field-value) -> result<_, header-error>; 244 | 245 | /// Retrieve the full set of names and values in the Fields. Like the 246 | /// constructor, the list represents each name-value pair. 247 | /// 248 | /// The outer list represents each name-value pair in the Fields. Names 249 | /// which have multiple values are represented by multiple entries in this 250 | /// list with the same name. 251 | /// 252 | /// The names and values are always returned in the original casing and in 253 | /// the order in which they will be serialized for transport. 254 | @since(version = 0.2.0) 255 | entries: func() -> list>; 256 | 257 | /// Make a deep copy of the Fields. Equivalent in behavior to calling the 258 | /// `fields` constructor on the return value of `entries`. The resulting 259 | /// `fields` is mutable. 260 | @since(version = 0.2.0) 261 | clone: func() -> fields; 262 | } 263 | 264 | /// Headers is an alias for Fields. 265 | @since(version = 0.2.0) 266 | type headers = fields; 267 | 268 | /// Trailers is an alias for Fields. 269 | @since(version = 0.2.0) 270 | type trailers = fields; 271 | 272 | /// Represents an incoming HTTP Request. 273 | @since(version = 0.2.0) 274 | resource incoming-request { 275 | 276 | /// Returns the method of the incoming request. 277 | @since(version = 0.2.0) 278 | method: func() -> method; 279 | 280 | /// Returns the path with query parameters from the request, as a string. 281 | @since(version = 0.2.0) 282 | path-with-query: func() -> option; 283 | 284 | /// Returns the protocol scheme from the request. 285 | @since(version = 0.2.0) 286 | scheme: func() -> option; 287 | 288 | /// Returns the authority of the Request's target URI, if present. 289 | @since(version = 0.2.0) 290 | authority: func() -> option; 291 | 292 | /// Get the `headers` associated with the request. 293 | /// 294 | /// The returned `headers` resource is immutable: `set`, `append`, and 295 | /// `delete` operations will fail with `header-error.immutable`. 296 | /// 297 | /// The `headers` returned are a child resource: it must be dropped before 298 | /// the parent `incoming-request` is dropped. Dropping this 299 | /// `incoming-request` before all children are dropped will trap. 300 | @since(version = 0.2.0) 301 | headers: func() -> headers; 302 | 303 | /// Gives the `incoming-body` associated with this request. Will only 304 | /// return success at most once, and subsequent calls will return error. 305 | @since(version = 0.2.0) 306 | consume: func() -> result; 307 | } 308 | 309 | /// Represents an outgoing HTTP Request. 310 | @since(version = 0.2.0) 311 | resource outgoing-request { 312 | 313 | /// Construct a new `outgoing-request` with a default `method` of `GET`, and 314 | /// `none` values for `path-with-query`, `scheme`, and `authority`. 315 | /// 316 | /// * `headers` is the HTTP Headers for the Request. 317 | /// 318 | /// It is possible to construct, or manipulate with the accessor functions 319 | /// below, an `outgoing-request` with an invalid combination of `scheme` 320 | /// and `authority`, or `headers` which are not permitted to be sent. 321 | /// It is the obligation of the `outgoing-handler.handle` implementation 322 | /// to reject invalid constructions of `outgoing-request`. 323 | @since(version = 0.2.0) 324 | constructor( 325 | headers: headers 326 | ); 327 | 328 | /// Returns the resource corresponding to the outgoing Body for this 329 | /// Request. 330 | /// 331 | /// Returns success on the first call: the `outgoing-body` resource for 332 | /// this `outgoing-request` can be retrieved at most once. Subsequent 333 | /// calls will return error. 334 | @since(version = 0.2.0) 335 | body: func() -> result; 336 | 337 | /// Get the Method for the Request. 338 | @since(version = 0.2.0) 339 | method: func() -> method; 340 | /// Set the Method for the Request. Fails if the string present in a 341 | /// `method.other` argument is not a syntactically valid method. 342 | @since(version = 0.2.0) 343 | set-method: func(method: method) -> result; 344 | 345 | /// Get the combination of the HTTP Path and Query for the Request. 346 | /// When `none`, this represents an empty Path and empty Query. 347 | @since(version = 0.2.0) 348 | path-with-query: func() -> option; 349 | /// Set the combination of the HTTP Path and Query for the Request. 350 | /// When `none`, this represents an empty Path and empty Query. Fails is the 351 | /// string given is not a syntactically valid path and query uri component. 352 | @since(version = 0.2.0) 353 | set-path-with-query: func(path-with-query: option) -> result; 354 | 355 | /// Get the HTTP Related Scheme for the Request. When `none`, the 356 | /// implementation may choose an appropriate default scheme. 357 | @since(version = 0.2.0) 358 | scheme: func() -> option; 359 | /// Set the HTTP Related Scheme for the Request. When `none`, the 360 | /// implementation may choose an appropriate default scheme. Fails if the 361 | /// string given is not a syntactically valid uri scheme. 362 | @since(version = 0.2.0) 363 | set-scheme: func(scheme: option) -> result; 364 | 365 | /// Get the authority of the Request's target URI. A value of `none` may be used 366 | /// with Related Schemes which do not require an authority. The HTTP and 367 | /// HTTPS schemes always require an authority. 368 | @since(version = 0.2.0) 369 | authority: func() -> option; 370 | /// Set the authority of the Request's target URI. A value of `none` may be used 371 | /// with Related Schemes which do not require an authority. The HTTP and 372 | /// HTTPS schemes always require an authority. Fails if the string given is 373 | /// not a syntactically valid URI authority. 374 | @since(version = 0.2.0) 375 | set-authority: func(authority: option) -> result; 376 | 377 | /// Get the headers associated with the Request. 378 | /// 379 | /// The returned `headers` resource is immutable: `set`, `append`, and 380 | /// `delete` operations will fail with `header-error.immutable`. 381 | /// 382 | /// This headers resource is a child: it must be dropped before the parent 383 | /// `outgoing-request` is dropped, or its ownership is transferred to 384 | /// another component by e.g. `outgoing-handler.handle`. 385 | @since(version = 0.2.0) 386 | headers: func() -> headers; 387 | } 388 | 389 | /// Parameters for making an HTTP Request. Each of these parameters is 390 | /// currently an optional timeout applicable to the transport layer of the 391 | /// HTTP protocol. 392 | /// 393 | /// These timeouts are separate from any the user may use to bound a 394 | /// blocking call to `wasi:io/poll.poll`. 395 | @since(version = 0.2.0) 396 | resource request-options { 397 | /// Construct a default `request-options` value. 398 | @since(version = 0.2.0) 399 | constructor(); 400 | 401 | /// The timeout for the initial connect to the HTTP Server. 402 | @since(version = 0.2.0) 403 | connect-timeout: func() -> option; 404 | 405 | /// Set the timeout for the initial connect to the HTTP Server. An error 406 | /// return value indicates that this timeout is not supported. 407 | @since(version = 0.2.0) 408 | set-connect-timeout: func(duration: option) -> result; 409 | 410 | /// The timeout for receiving the first byte of the Response body. 411 | @since(version = 0.2.0) 412 | first-byte-timeout: func() -> option; 413 | 414 | /// Set the timeout for receiving the first byte of the Response body. An 415 | /// error return value indicates that this timeout is not supported. 416 | @since(version = 0.2.0) 417 | set-first-byte-timeout: func(duration: option) -> result; 418 | 419 | /// The timeout for receiving subsequent chunks of bytes in the Response 420 | /// body stream. 421 | @since(version = 0.2.0) 422 | between-bytes-timeout: func() -> option; 423 | 424 | /// Set the timeout for receiving subsequent chunks of bytes in the Response 425 | /// body stream. An error return value indicates that this timeout is not 426 | /// supported. 427 | @since(version = 0.2.0) 428 | set-between-bytes-timeout: func(duration: option) -> result; 429 | } 430 | 431 | /// Represents the ability to send an HTTP Response. 432 | /// 433 | /// This resource is used by the `wasi:http/incoming-handler` interface to 434 | /// allow a Response to be sent corresponding to the Request provided as the 435 | /// other argument to `incoming-handler.handle`. 436 | @since(version = 0.2.0) 437 | resource response-outparam { 438 | 439 | /// Set the value of the `response-outparam` to either send a response, 440 | /// or indicate an error. 441 | /// 442 | /// This method consumes the `response-outparam` to ensure that it is 443 | /// called at most once. If it is never called, the implementation 444 | /// will respond with an error. 445 | /// 446 | /// The user may provide an `error` to `response` to allow the 447 | /// implementation determine how to respond with an HTTP error response. 448 | @since(version = 0.2.0) 449 | set: static func( 450 | param: response-outparam, 451 | response: result, 452 | ); 453 | } 454 | 455 | /// This type corresponds to the HTTP standard Status Code. 456 | @since(version = 0.2.0) 457 | type status-code = u16; 458 | 459 | /// Represents an incoming HTTP Response. 460 | @since(version = 0.2.0) 461 | resource incoming-response { 462 | 463 | /// Returns the status code from the incoming response. 464 | @since(version = 0.2.0) 465 | status: func() -> status-code; 466 | 467 | /// Returns the headers from the incoming response. 468 | /// 469 | /// The returned `headers` resource is immutable: `set`, `append`, and 470 | /// `delete` operations will fail with `header-error.immutable`. 471 | /// 472 | /// This headers resource is a child: it must be dropped before the parent 473 | /// `incoming-response` is dropped. 474 | @since(version = 0.2.0) 475 | headers: func() -> headers; 476 | 477 | /// Returns the incoming body. May be called at most once. Returns error 478 | /// if called additional times. 479 | @since(version = 0.2.0) 480 | consume: func() -> result; 481 | } 482 | 483 | /// Represents an incoming HTTP Request or Response's Body. 484 | /// 485 | /// A body has both its contents - a stream of bytes - and a (possibly 486 | /// empty) set of trailers, indicating that the full contents of the 487 | /// body have been received. This resource represents the contents as 488 | /// an `input-stream` and the delivery of trailers as a `future-trailers`, 489 | /// and ensures that the user of this interface may only be consuming either 490 | /// the body contents or waiting on trailers at any given time. 491 | @since(version = 0.2.0) 492 | resource incoming-body { 493 | 494 | /// Returns the contents of the body, as a stream of bytes. 495 | /// 496 | /// Returns success on first call: the stream representing the contents 497 | /// can be retrieved at most once. Subsequent calls will return error. 498 | /// 499 | /// The returned `input-stream` resource is a child: it must be dropped 500 | /// before the parent `incoming-body` is dropped, or consumed by 501 | /// `incoming-body.finish`. 502 | /// 503 | /// This invariant ensures that the implementation can determine whether 504 | /// the user is consuming the contents of the body, waiting on the 505 | /// `future-trailers` to be ready, or neither. This allows for network 506 | /// backpressure is to be applied when the user is consuming the body, 507 | /// and for that backpressure to not inhibit delivery of the trailers if 508 | /// the user does not read the entire body. 509 | @since(version = 0.2.0) 510 | %stream: func() -> result; 511 | 512 | /// Takes ownership of `incoming-body`, and returns a `future-trailers`. 513 | /// This function will trap if the `input-stream` child is still alive. 514 | @since(version = 0.2.0) 515 | finish: static func(this: incoming-body) -> future-trailers; 516 | } 517 | 518 | /// Represents a future which may eventually return trailers, or an error. 519 | /// 520 | /// In the case that the incoming HTTP Request or Response did not have any 521 | /// trailers, this future will resolve to the empty set of trailers once the 522 | /// complete Request or Response body has been received. 523 | @since(version = 0.2.0) 524 | resource future-trailers { 525 | 526 | /// Returns a pollable which becomes ready when either the trailers have 527 | /// been received, or an error has occurred. When this pollable is ready, 528 | /// the `get` method will return `some`. 529 | @since(version = 0.2.0) 530 | subscribe: func() -> pollable; 531 | 532 | /// Returns the contents of the trailers, or an error which occurred, 533 | /// once the future is ready. 534 | /// 535 | /// The outer `option` represents future readiness. Users can wait on this 536 | /// `option` to become `some` using the `subscribe` method. 537 | /// 538 | /// The outer `result` is used to retrieve the trailers or error at most 539 | /// once. It will be success on the first call in which the outer option 540 | /// is `some`, and error on subsequent calls. 541 | /// 542 | /// The inner `result` represents that either the HTTP Request or Response 543 | /// body, as well as any trailers, were received successfully, or that an 544 | /// error occurred receiving them. The optional `trailers` indicates whether 545 | /// or not trailers were present in the body. 546 | /// 547 | /// When some `trailers` are returned by this method, the `trailers` 548 | /// resource is immutable, and a child. Use of the `set`, `append`, or 549 | /// `delete` methods will return an error, and the resource must be 550 | /// dropped before the parent `future-trailers` is dropped. 551 | @since(version = 0.2.0) 552 | get: func() -> option, error-code>>>; 553 | } 554 | 555 | /// Represents an outgoing HTTP Response. 556 | @since(version = 0.2.0) 557 | resource outgoing-response { 558 | 559 | /// Construct an `outgoing-response`, with a default `status-code` of `200`. 560 | /// If a different `status-code` is needed, it must be set via the 561 | /// `set-status-code` method. 562 | /// 563 | /// * `headers` is the HTTP Headers for the Response. 564 | @since(version = 0.2.0) 565 | constructor(headers: headers); 566 | 567 | /// Get the HTTP Status Code for the Response. 568 | @since(version = 0.2.0) 569 | status-code: func() -> status-code; 570 | 571 | /// Set the HTTP Status Code for the Response. Fails if the status-code 572 | /// given is not a valid http status code. 573 | @since(version = 0.2.0) 574 | set-status-code: func(status-code: status-code) -> result; 575 | 576 | /// Get the headers associated with the Request. 577 | /// 578 | /// The returned `headers` resource is immutable: `set`, `append`, and 579 | /// `delete` operations will fail with `header-error.immutable`. 580 | /// 581 | /// This headers resource is a child: it must be dropped before the parent 582 | /// `outgoing-request` is dropped, or its ownership is transferred to 583 | /// another component by e.g. `outgoing-handler.handle`. 584 | @since(version = 0.2.0) 585 | headers: func() -> headers; 586 | 587 | /// Returns the resource corresponding to the outgoing Body for this Response. 588 | /// 589 | /// Returns success on the first call: the `outgoing-body` resource for 590 | /// this `outgoing-response` can be retrieved at most once. Subsequent 591 | /// calls will return error. 592 | @since(version = 0.2.0) 593 | body: func() -> result; 594 | } 595 | 596 | /// Represents an outgoing HTTP Request or Response's Body. 597 | /// 598 | /// A body has both its contents - a stream of bytes - and a (possibly 599 | /// empty) set of trailers, inducating the full contents of the body 600 | /// have been sent. This resource represents the contents as an 601 | /// `output-stream` child resource, and the completion of the body (with 602 | /// optional trailers) with a static function that consumes the 603 | /// `outgoing-body` resource, and ensures that the user of this interface 604 | /// may not write to the body contents after the body has been finished. 605 | /// 606 | /// If the user code drops this resource, as opposed to calling the static 607 | /// method `finish`, the implementation should treat the body as incomplete, 608 | /// and that an error has occurred. The implementation should propagate this 609 | /// error to the HTTP protocol by whatever means it has available, 610 | /// including: corrupting the body on the wire, aborting the associated 611 | /// Request, or sending a late status code for the Response. 612 | @since(version = 0.2.0) 613 | resource outgoing-body { 614 | 615 | /// Returns a stream for writing the body contents. 616 | /// 617 | /// The returned `output-stream` is a child resource: it must be dropped 618 | /// before the parent `outgoing-body` resource is dropped (or finished), 619 | /// otherwise the `outgoing-body` drop or `finish` will trap. 620 | /// 621 | /// Returns success on the first call: the `output-stream` resource for 622 | /// this `outgoing-body` may be retrieved at most once. Subsequent calls 623 | /// will return error. 624 | @since(version = 0.2.0) 625 | write: func() -> result; 626 | 627 | /// Finalize an outgoing body, optionally providing trailers. This must be 628 | /// called to signal that the response is complete. If the `outgoing-body` 629 | /// is dropped without calling `outgoing-body.finalize`, the implementation 630 | /// should treat the body as corrupted. 631 | /// 632 | /// Fails if the body's `outgoing-request` or `outgoing-response` was 633 | /// constructed with a Content-Length header, and the contents written 634 | /// to the body (via `write`) does not match the value given in the 635 | /// Content-Length. 636 | @since(version = 0.2.0) 637 | finish: static func( 638 | this: outgoing-body, 639 | trailers: option 640 | ) -> result<_, error-code>; 641 | } 642 | 643 | /// Represents a future which may eventually return an incoming HTTP 644 | /// Response, or an error. 645 | /// 646 | /// This resource is returned by the `wasi:http/outgoing-handler` interface to 647 | /// provide the HTTP Response corresponding to the sent Request. 648 | @since(version = 0.2.0) 649 | resource future-incoming-response { 650 | /// Returns a pollable which becomes ready when either the Response has 651 | /// been received, or an error has occurred. When this pollable is ready, 652 | /// the `get` method will return `some`. 653 | @since(version = 0.2.0) 654 | subscribe: func() -> pollable; 655 | 656 | /// Returns the incoming HTTP Response, or an error, once one is ready. 657 | /// 658 | /// The outer `option` represents future readiness. Users can wait on this 659 | /// `option` to become `some` using the `subscribe` method. 660 | /// 661 | /// The outer `result` is used to retrieve the response or error at most 662 | /// once. It will be success on the first call in which the outer option 663 | /// is `some`, and error on subsequent calls. 664 | /// 665 | /// The inner `result` represents that either the incoming HTTP Response 666 | /// status and headers have received successfully, or that an error 667 | /// occurred. Errors may also occur while consuming the response body, 668 | /// but those will be reported by the `incoming-body` and its 669 | /// `output-stream` child. 670 | @since(version = 0.2.0) 671 | get: func() -> option>>; 672 | } 673 | } 674 | -------------------------------------------------------------------------------- /waki/wit/deps/io/error.wit: -------------------------------------------------------------------------------- 1 | package wasi:io@0.2.2; 2 | 3 | @since(version = 0.2.0) 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 | /// offer functions to "downcast" this error into more specific types. For example, 15 | /// errors returned from streams derived from filesystem types can be described using 16 | /// the filesystem's own error-code type. This is done using the function 17 | /// `wasi:filesystem/types/filesystem-error-code`, which takes a `borrow` 18 | /// parameter and returns an `option`. 19 | /// 20 | /// The set of functions which can "downcast" an `error` into a more 21 | /// concrete type is open. 22 | @since(version = 0.2.0) 23 | resource error { 24 | /// Returns a string that is suitable to assist humans in debugging 25 | /// this error. 26 | /// 27 | /// WARNING: The returned string should not be consumed mechanically! 28 | /// It may change across platforms, hosts, or other implementation 29 | /// details. Parsing this string is a major platform-compatibility 30 | /// hazard. 31 | @since(version = 0.2.0) 32 | to-debug-string: func() -> string; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /waki/wit/deps/io/poll.wit: -------------------------------------------------------------------------------- 1 | package wasi:io@0.2.2; 2 | 3 | /// A poll API intended to let users wait for I/O events on multiple handles 4 | /// at once. 5 | @since(version = 0.2.0) 6 | interface poll { 7 | /// `pollable` represents a single I/O event which may be ready, or not. 8 | @since(version = 0.2.0) 9 | resource pollable { 10 | 11 | /// Return the readiness of a pollable. This function never blocks. 12 | /// 13 | /// Returns `true` when the pollable is ready, and `false` otherwise. 14 | @since(version = 0.2.0) 15 | ready: func() -> bool; 16 | 17 | /// `block` returns immediately if the pollable is ready, and otherwise 18 | /// blocks until ready. 19 | /// 20 | /// This function is equivalent to calling `poll.poll` on a list 21 | /// containing only this pollable. 22 | @since(version = 0.2.0) 23 | block: func(); 24 | } 25 | 26 | /// Poll for completion on a set of pollables. 27 | /// 28 | /// This function takes a list of pollables, which identify I/O sources of 29 | /// interest, and waits until one or more of the events is ready for I/O. 30 | /// 31 | /// The result `list` contains one or more indices of handles in the 32 | /// argument list that is ready for I/O. 33 | /// 34 | /// This function traps if either: 35 | /// - the list is empty, or: 36 | /// - the list contains more elements than can be indexed with a `u32` value. 37 | /// 38 | /// A timeout can be implemented by adding a pollable from the 39 | /// wasi-clocks API to the list. 40 | /// 41 | /// This function does not return a `result`; polling in itself does not 42 | /// do any I/O so it doesn't fail. If any of the I/O sources identified by 43 | /// the pollables has an error, it is indicated by marking the source as 44 | /// being ready for I/O. 45 | @since(version = 0.2.0) 46 | poll: func(in: list>) -> list; 47 | } 48 | -------------------------------------------------------------------------------- /waki/wit/deps/io/streams.wit: -------------------------------------------------------------------------------- 1 | package wasi:io@0.2.2; 2 | 3 | /// WASI I/O is an I/O abstraction API which is currently focused on providing 4 | /// stream types. 5 | /// 6 | /// In the future, the component model is expected to add built-in stream types; 7 | /// when it does, they are expected to subsume this API. 8 | @since(version = 0.2.0) 9 | interface streams { 10 | @since(version = 0.2.0) 11 | use error.{error}; 12 | @since(version = 0.2.0) 13 | use poll.{pollable}; 14 | 15 | /// An error for input-stream and output-stream operations. 16 | @since(version = 0.2.0) 17 | variant stream-error { 18 | /// The last operation (a write or flush) failed before completion. 19 | /// 20 | /// More information is available in the `error` payload. 21 | /// 22 | /// After this, the stream will be closed. All future operations return 23 | /// `stream-error::closed`. 24 | last-operation-failed(error), 25 | /// The stream is closed: no more input will be accepted by the 26 | /// stream. A closed output-stream will return this error on all 27 | /// future operations. 28 | closed 29 | } 30 | 31 | /// An input bytestream. 32 | /// 33 | /// `input-stream`s are *non-blocking* to the extent practical on underlying 34 | /// platforms. I/O operations always return promptly; if fewer bytes are 35 | /// promptly available than requested, they return the number of bytes promptly 36 | /// available, which could even be zero. To wait for data to be available, 37 | /// use the `subscribe` function to obtain a `pollable` which can be polled 38 | /// for using `wasi:io/poll`. 39 | @since(version = 0.2.0) 40 | resource input-stream { 41 | /// Perform a non-blocking read from the stream. 42 | /// 43 | /// When the source of a `read` is binary data, the bytes from the source 44 | /// are returned verbatim. When the source of a `read` is known to the 45 | /// implementation to be text, bytes containing the UTF-8 encoding of the 46 | /// text are returned. 47 | /// 48 | /// This function returns a list of bytes containing the read data, 49 | /// when successful. The returned list will contain up to `len` bytes; 50 | /// it may return fewer than requested, but not more. The list is 51 | /// empty when no bytes are available for reading at this time. The 52 | /// pollable given by `subscribe` will be ready when more bytes are 53 | /// available. 54 | /// 55 | /// This function fails with a `stream-error` when the operation 56 | /// encounters an error, giving `last-operation-failed`, or when the 57 | /// stream is closed, giving `closed`. 58 | /// 59 | /// When the caller gives a `len` of 0, it represents a request to 60 | /// read 0 bytes. If the stream is still open, this call should 61 | /// succeed and return an empty list, or otherwise fail with `closed`. 62 | /// 63 | /// The `len` parameter is a `u64`, which could represent a list of u8 which 64 | /// is not possible to allocate in wasm32, or not desirable to allocate as 65 | /// as a return value by the callee. The callee may return a list of bytes 66 | /// less than `len` in size while more bytes are available for reading. 67 | @since(version = 0.2.0) 68 | read: func( 69 | /// The maximum number of bytes to read 70 | len: u64 71 | ) -> result, stream-error>; 72 | 73 | /// Read bytes from a stream, after blocking until at least one byte can 74 | /// be read. Except for blocking, behavior is identical to `read`. 75 | @since(version = 0.2.0) 76 | blocking-read: func( 77 | /// The maximum number of bytes to read 78 | len: u64 79 | ) -> result, stream-error>; 80 | 81 | /// Skip bytes from a stream. Returns number of bytes skipped. 82 | /// 83 | /// Behaves identical to `read`, except instead of returning a list 84 | /// of bytes, returns the number of bytes consumed from the stream. 85 | @since(version = 0.2.0) 86 | skip: func( 87 | /// The maximum number of bytes to skip. 88 | len: u64, 89 | ) -> result; 90 | 91 | /// Skip bytes from a stream, after blocking until at least one byte 92 | /// can be skipped. Except for blocking behavior, identical to `skip`. 93 | @since(version = 0.2.0) 94 | blocking-skip: func( 95 | /// The maximum number of bytes to skip. 96 | len: u64, 97 | ) -> result; 98 | 99 | /// Create a `pollable` which will resolve once either the specified stream 100 | /// has bytes available to read or the other end of the stream has been 101 | /// closed. 102 | /// The created `pollable` is a child resource of the `input-stream`. 103 | /// Implementations may trap if the `input-stream` is dropped before 104 | /// all derived `pollable`s created with this function are dropped. 105 | @since(version = 0.2.0) 106 | subscribe: func() -> pollable; 107 | } 108 | 109 | 110 | /// An output bytestream. 111 | /// 112 | /// `output-stream`s are *non-blocking* to the extent practical on 113 | /// underlying platforms. Except where specified otherwise, I/O operations also 114 | /// always return promptly, after the number of bytes that can be written 115 | /// promptly, which could even be zero. To wait for the stream to be ready to 116 | /// accept data, the `subscribe` function to obtain a `pollable` which can be 117 | /// polled for using `wasi:io/poll`. 118 | /// 119 | /// Dropping an `output-stream` while there's still an active write in 120 | /// progress may result in the data being lost. Before dropping the stream, 121 | /// be sure to fully flush your writes. 122 | @since(version = 0.2.0) 123 | resource output-stream { 124 | /// Check readiness for writing. This function never blocks. 125 | /// 126 | /// Returns the number of bytes permitted for the next call to `write`, 127 | /// or an error. Calling `write` with more bytes than this function has 128 | /// permitted will trap. 129 | /// 130 | /// When this function returns 0 bytes, the `subscribe` pollable will 131 | /// become ready when this function will report at least 1 byte, or an 132 | /// error. 133 | @since(version = 0.2.0) 134 | check-write: func() -> result; 135 | 136 | /// Perform a write. This function never blocks. 137 | /// 138 | /// When the destination of a `write` is binary data, the bytes from 139 | /// `contents` are written verbatim. When the destination of a `write` is 140 | /// known to the implementation to be text, the bytes of `contents` are 141 | /// transcoded from UTF-8 into the encoding of the destination and then 142 | /// written. 143 | /// 144 | /// Precondition: check-write gave permit of Ok(n) and contents has a 145 | /// length of less than or equal to n. Otherwise, this function will trap. 146 | /// 147 | /// returns Err(closed) without writing if the stream has closed since 148 | /// the last call to check-write provided a permit. 149 | @since(version = 0.2.0) 150 | write: func( 151 | contents: list 152 | ) -> result<_, stream-error>; 153 | 154 | /// Perform a write of up to 4096 bytes, and then flush the stream. Block 155 | /// until all of these operations are complete, or an error occurs. 156 | /// 157 | /// This is a convenience wrapper around the use of `check-write`, 158 | /// `subscribe`, `write`, and `flush`, and is implemented with the 159 | /// following pseudo-code: 160 | /// 161 | /// ```text 162 | /// let pollable = this.subscribe(); 163 | /// while !contents.is_empty() { 164 | /// // Wait for the stream to become writable 165 | /// pollable.block(); 166 | /// let Ok(n) = this.check-write(); // eliding error handling 167 | /// let len = min(n, contents.len()); 168 | /// let (chunk, rest) = contents.split_at(len); 169 | /// this.write(chunk ); // eliding error handling 170 | /// contents = rest; 171 | /// } 172 | /// this.flush(); 173 | /// // Wait for completion of `flush` 174 | /// pollable.block(); 175 | /// // Check for any errors that arose during `flush` 176 | /// let _ = this.check-write(); // eliding error handling 177 | /// ``` 178 | @since(version = 0.2.0) 179 | blocking-write-and-flush: func( 180 | contents: list 181 | ) -> result<_, stream-error>; 182 | 183 | /// Request to flush buffered output. This function never blocks. 184 | /// 185 | /// This tells the output-stream that the caller intends any buffered 186 | /// output to be flushed. the output which is expected to be flushed 187 | /// is all that has been passed to `write` prior to this call. 188 | /// 189 | /// Upon calling this function, the `output-stream` will not accept any 190 | /// writes (`check-write` will return `ok(0)`) until the flush has 191 | /// completed. The `subscribe` pollable will become ready when the 192 | /// flush has completed and the stream can accept more writes. 193 | @since(version = 0.2.0) 194 | flush: func() -> result<_, stream-error>; 195 | 196 | /// Request to flush buffered output, and block until flush completes 197 | /// and stream is ready for writing again. 198 | @since(version = 0.2.0) 199 | blocking-flush: func() -> result<_, stream-error>; 200 | 201 | /// Create a `pollable` which will resolve once the output-stream 202 | /// is ready for more writing, or an error has occurred. When this 203 | /// pollable is ready, `check-write` will return `ok(n)` with n>0, or an 204 | /// error. 205 | /// 206 | /// If the stream is closed, this pollable is always ready immediately. 207 | /// 208 | /// The created `pollable` is a child resource of the `output-stream`. 209 | /// Implementations may trap if the `output-stream` is dropped before 210 | /// all derived `pollable`s created with this function are dropped. 211 | @since(version = 0.2.0) 212 | subscribe: func() -> pollable; 213 | 214 | /// Write zeroes to a stream. 215 | /// 216 | /// This should be used precisely like `write` with the exact same 217 | /// preconditions (must use check-write first), but instead of 218 | /// passing a list of bytes, you simply pass the number of zero-bytes 219 | /// that should be written. 220 | @since(version = 0.2.0) 221 | write-zeroes: func( 222 | /// The number of zero-bytes to write 223 | len: u64 224 | ) -> result<_, stream-error>; 225 | 226 | /// Perform a write of up to 4096 zeroes, and then flush the stream. 227 | /// Block until all of these operations are complete, or an error 228 | /// occurs. 229 | /// 230 | /// This is a convenience wrapper around the use of `check-write`, 231 | /// `subscribe`, `write-zeroes`, and `flush`, and is implemented with 232 | /// the following pseudo-code: 233 | /// 234 | /// ```text 235 | /// let pollable = this.subscribe(); 236 | /// while num_zeroes != 0 { 237 | /// // Wait for the stream to become writable 238 | /// pollable.block(); 239 | /// let Ok(n) = this.check-write(); // eliding error handling 240 | /// let len = min(n, num_zeroes); 241 | /// this.write-zeroes(len); // eliding error handling 242 | /// num_zeroes -= len; 243 | /// } 244 | /// this.flush(); 245 | /// // Wait for completion of `flush` 246 | /// pollable.block(); 247 | /// // Check for any errors that arose during `flush` 248 | /// let _ = this.check-write(); // eliding error handling 249 | /// ``` 250 | @since(version = 0.2.0) 251 | blocking-write-zeroes-and-flush: func( 252 | /// The number of zero-bytes to write 253 | len: u64 254 | ) -> result<_, stream-error>; 255 | 256 | /// Read from one stream and write to another. 257 | /// 258 | /// The behavior of splice is equivalent to: 259 | /// 1. calling `check-write` on the `output-stream` 260 | /// 2. calling `read` on the `input-stream` with the smaller of the 261 | /// `check-write` permitted length and the `len` provided to `splice` 262 | /// 3. calling `write` on the `output-stream` with that read data. 263 | /// 264 | /// Any error reported by the call to `check-write`, `read`, or 265 | /// `write` ends the splice and reports that error. 266 | /// 267 | /// This function returns the number of bytes transferred; it may be less 268 | /// than `len`. 269 | @since(version = 0.2.0) 270 | splice: func( 271 | /// The stream to read from 272 | src: borrow, 273 | /// The number of bytes to splice 274 | len: u64, 275 | ) -> result; 276 | 277 | /// Read from one stream and write to another, with blocking. 278 | /// 279 | /// This is similar to `splice`, except that it blocks until the 280 | /// `output-stream` is ready for writing, and the `input-stream` 281 | /// is ready for reading, before performing the `splice`. 282 | @since(version = 0.2.0) 283 | blocking-splice: func( 284 | /// The stream to read from 285 | src: borrow, 286 | /// The number of bytes to splice 287 | len: u64, 288 | ) -> result; 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /waki/wit/deps/io/world.wit: -------------------------------------------------------------------------------- 1 | package wasi:io@0.2.2; 2 | 3 | @since(version = 0.2.0) 4 | world imports { 5 | @since(version = 0.2.0) 6 | import streams; 7 | 8 | @since(version = 0.2.0) 9 | import poll; 10 | } 11 | -------------------------------------------------------------------------------- /waki/wit/deps/random/insecure-seed.wit: -------------------------------------------------------------------------------- 1 | package wasi:random@0.2.2; 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 | @since(version = 0.2.0) 7 | interface insecure-seed { 8 | /// Return a 128-bit value that may contain a pseudo-random value. 9 | /// 10 | /// The returned value is not required to be computed from a CSPRNG, and may 11 | /// even be entirely deterministic. Host implementations are encouraged to 12 | /// provide pseudo-random values to any program exposed to 13 | /// attacker-controlled content, to enable DoS protection built into many 14 | /// languages' hash-map implementations. 15 | /// 16 | /// This function is intended to only be called once, by a source language 17 | /// to initialize Denial Of Service (DoS) protection in its hash-map 18 | /// implementation. 19 | /// 20 | /// # Expected future evolution 21 | /// 22 | /// This will likely be changed to a value import, to prevent it from being 23 | /// called multiple times and potentially used for purposes other than DoS 24 | /// protection. 25 | @since(version = 0.2.0) 26 | insecure-seed: func() -> tuple; 27 | } 28 | -------------------------------------------------------------------------------- /waki/wit/deps/random/insecure.wit: -------------------------------------------------------------------------------- 1 | package wasi:random@0.2.2; 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 | @since(version = 0.2.0) 7 | interface insecure { 8 | /// Return `len` insecure pseudo-random bytes. 9 | /// 10 | /// This function is not cryptographically secure. Do not use it for 11 | /// anything related to security. 12 | /// 13 | /// There are no requirements on the values of the returned bytes, however 14 | /// implementations are encouraged to return evenly distributed values with 15 | /// a long period. 16 | @since(version = 0.2.0) 17 | get-insecure-random-bytes: func(len: u64) -> list; 18 | 19 | /// Return an insecure pseudo-random `u64` value. 20 | /// 21 | /// This function returns the same type of pseudo-random data as 22 | /// `get-insecure-random-bytes`, represented as a `u64`. 23 | @since(version = 0.2.0) 24 | get-insecure-random-u64: func() -> u64; 25 | } 26 | -------------------------------------------------------------------------------- /waki/wit/deps/random/random.wit: -------------------------------------------------------------------------------- 1 | package wasi:random@0.2.2; 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 | @since(version = 0.2.0) 7 | interface random { 8 | /// Return `len` cryptographically-secure random or pseudo-random bytes. 9 | /// 10 | /// This function must produce data at least as cryptographically secure and 11 | /// fast as an adequately seeded cryptographically-secure pseudo-random 12 | /// number generator (CSPRNG). It must not block, from the perspective of 13 | /// the calling program, under any circumstances, including on the first 14 | /// request and on requests for numbers of bytes. The returned data must 15 | /// always be unpredictable. 16 | /// 17 | /// This function must always return fresh data. Deterministic environments 18 | /// must omit this function, rather than implementing it with deterministic 19 | /// data. 20 | @since(version = 0.2.0) 21 | get-random-bytes: func(len: u64) -> list; 22 | 23 | /// Return a cryptographically-secure random or pseudo-random `u64` value. 24 | /// 25 | /// This function returns the same type of data as `get-random-bytes`, 26 | /// represented as a `u64`. 27 | @since(version = 0.2.0) 28 | get-random-u64: func() -> u64; 29 | } 30 | -------------------------------------------------------------------------------- /waki/wit/deps/random/world.wit: -------------------------------------------------------------------------------- 1 | package wasi:random@0.2.2; 2 | 3 | @since(version = 0.2.0) 4 | world imports { 5 | @since(version = 0.2.0) 6 | import random; 7 | 8 | @since(version = 0.2.0) 9 | import insecure; 10 | 11 | @since(version = 0.2.0) 12 | import insecure-seed; 13 | } 14 | -------------------------------------------------------------------------------- /waki/wit/deps/sockets/instance-network.wit: -------------------------------------------------------------------------------- 1 | 2 | /// This interface provides a value-export of the default network handle.. 3 | @since(version = 0.2.0) 4 | interface instance-network { 5 | @since(version = 0.2.0) 6 | use network.{network}; 7 | 8 | /// Get a handle to the default network. 9 | @since(version = 0.2.0) 10 | instance-network: func() -> network; 11 | } 12 | -------------------------------------------------------------------------------- /waki/wit/deps/sockets/ip-name-lookup.wit: -------------------------------------------------------------------------------- 1 | @since(version = 0.2.0) 2 | interface ip-name-lookup { 3 | @since(version = 0.2.0) 4 | use wasi:io/poll@0.2.2.{pollable}; 5 | @since(version = 0.2.0) 6 | use network.{network, error-code, ip-address}; 7 | 8 | /// Resolve an internet host name to a list of IP addresses. 9 | /// 10 | /// Unicode domain names are automatically converted to ASCII using IDNA encoding. 11 | /// If the input is an IP address string, the address is parsed and returned 12 | /// as-is without making any external requests. 13 | /// 14 | /// See the wasi-socket proposal README.md for a comparison with getaddrinfo. 15 | /// 16 | /// This function never blocks. It either immediately fails or immediately 17 | /// returns successfully with a `resolve-address-stream` that can be used 18 | /// to (asynchronously) fetch the results. 19 | /// 20 | /// # Typical errors 21 | /// - `invalid-argument`: `name` is a syntactically invalid domain name or IP address. 22 | /// 23 | /// # References: 24 | /// - 25 | /// - 26 | /// - 27 | /// - 28 | @since(version = 0.2.0) 29 | resolve-addresses: func(network: borrow, name: string) -> result; 30 | 31 | @since(version = 0.2.0) 32 | resource resolve-address-stream { 33 | /// Returns the next address from the resolver. 34 | /// 35 | /// This function should be called multiple times. On each call, it will 36 | /// return the next address in connection order preference. If all 37 | /// addresses have been exhausted, this function returns `none`. 38 | /// 39 | /// This function never returns IPv4-mapped IPv6 addresses. 40 | /// 41 | /// # Typical errors 42 | /// - `name-unresolvable`: Name does not exist or has no suitable associated IP addresses. (EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY) 43 | /// - `temporary-resolver-failure`: A temporary failure in name resolution occurred. (EAI_AGAIN) 44 | /// - `permanent-resolver-failure`: A permanent failure in name resolution occurred. (EAI_FAIL) 45 | /// - `would-block`: A result is not available yet. (EWOULDBLOCK, EAGAIN) 46 | @since(version = 0.2.0) 47 | resolve-next-address: func() -> result, error-code>; 48 | 49 | /// Create a `pollable` which will resolve once the stream is ready for I/O. 50 | /// 51 | /// Note: this function is here for WASI 0.2 only. 52 | /// It's planned to be removed when `future` is natively supported in Preview3. 53 | @since(version = 0.2.0) 54 | subscribe: func() -> pollable; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /waki/wit/deps/sockets/network.wit: -------------------------------------------------------------------------------- 1 | @since(version = 0.2.0) 2 | interface network { 3 | @unstable(feature = network-error-code) 4 | use wasi:io/error@0.2.2.{error}; 5 | 6 | /// An opaque resource that represents access to (a subset of) the network. 7 | /// This enables context-based security for networking. 8 | /// There is no need for this to map 1:1 to a physical network interface. 9 | @since(version = 0.2.0) 10 | resource network; 11 | 12 | /// Error codes. 13 | /// 14 | /// In theory, every API can return any error code. 15 | /// In practice, API's typically only return the errors documented per API 16 | /// combined with a couple of errors that are always possible: 17 | /// - `unknown` 18 | /// - `access-denied` 19 | /// - `not-supported` 20 | /// - `out-of-memory` 21 | /// - `concurrency-conflict` 22 | /// 23 | /// See each individual API for what the POSIX equivalents are. They sometimes differ per API. 24 | @since(version = 0.2.0) 25 | enum error-code { 26 | /// Unknown error 27 | unknown, 28 | 29 | /// Access denied. 30 | /// 31 | /// POSIX equivalent: EACCES, EPERM 32 | access-denied, 33 | 34 | /// The operation is not supported. 35 | /// 36 | /// POSIX equivalent: EOPNOTSUPP 37 | not-supported, 38 | 39 | /// One of the arguments is invalid. 40 | /// 41 | /// POSIX equivalent: EINVAL 42 | invalid-argument, 43 | 44 | /// Not enough memory to complete the operation. 45 | /// 46 | /// POSIX equivalent: ENOMEM, ENOBUFS, EAI_MEMORY 47 | out-of-memory, 48 | 49 | /// The operation timed out before it could finish completely. 50 | timeout, 51 | 52 | /// This operation is incompatible with another asynchronous operation that is already in progress. 53 | /// 54 | /// POSIX equivalent: EALREADY 55 | concurrency-conflict, 56 | 57 | /// Trying to finish an asynchronous operation that: 58 | /// - has not been started yet, or: 59 | /// - was already finished by a previous `finish-*` call. 60 | /// 61 | /// Note: this is scheduled to be removed when `future`s are natively supported. 62 | not-in-progress, 63 | 64 | /// The operation has been aborted because it could not be completed immediately. 65 | /// 66 | /// Note: this is scheduled to be removed when `future`s are natively supported. 67 | would-block, 68 | 69 | 70 | /// The operation is not valid in the socket's current state. 71 | invalid-state, 72 | 73 | /// A new socket resource could not be created because of a system limit. 74 | new-socket-limit, 75 | 76 | /// A bind operation failed because the provided address is not an address that the `network` can bind to. 77 | address-not-bindable, 78 | 79 | /// A bind operation failed because the provided address is already in use or because there are no ephemeral ports available. 80 | address-in-use, 81 | 82 | /// The remote address is not reachable 83 | remote-unreachable, 84 | 85 | 86 | /// The TCP connection was forcefully rejected 87 | connection-refused, 88 | 89 | /// The TCP connection was reset. 90 | connection-reset, 91 | 92 | /// A TCP connection was aborted. 93 | connection-aborted, 94 | 95 | 96 | /// The size of a datagram sent to a UDP socket exceeded the maximum 97 | /// supported size. 98 | datagram-too-large, 99 | 100 | 101 | /// Name does not exist or has no suitable associated IP addresses. 102 | name-unresolvable, 103 | 104 | /// A temporary failure in name resolution occurred. 105 | temporary-resolver-failure, 106 | 107 | /// A permanent failure in name resolution occurred. 108 | permanent-resolver-failure, 109 | } 110 | 111 | /// Attempts to extract a network-related `error-code` from the stream 112 | /// `error` provided. 113 | /// 114 | /// Stream operations which return `stream-error::last-operation-failed` 115 | /// have a payload with more information about the operation that failed. 116 | /// This payload can be passed through to this function to see if there's 117 | /// network-related information about the error to return. 118 | /// 119 | /// Note that this function is fallible because not all stream-related 120 | /// errors are network-related errors. 121 | @unstable(feature = network-error-code) 122 | network-error-code: func(err: borrow) -> option; 123 | 124 | @since(version = 0.2.0) 125 | enum ip-address-family { 126 | /// Similar to `AF_INET` in POSIX. 127 | ipv4, 128 | 129 | /// Similar to `AF_INET6` in POSIX. 130 | ipv6, 131 | } 132 | 133 | @since(version = 0.2.0) 134 | type ipv4-address = tuple; 135 | @since(version = 0.2.0) 136 | type ipv6-address = tuple; 137 | 138 | @since(version = 0.2.0) 139 | variant ip-address { 140 | ipv4(ipv4-address), 141 | ipv6(ipv6-address), 142 | } 143 | 144 | @since(version = 0.2.0) 145 | record ipv4-socket-address { 146 | /// sin_port 147 | port: u16, 148 | /// sin_addr 149 | address: ipv4-address, 150 | } 151 | 152 | @since(version = 0.2.0) 153 | record ipv6-socket-address { 154 | /// sin6_port 155 | port: u16, 156 | /// sin6_flowinfo 157 | flow-info: u32, 158 | /// sin6_addr 159 | address: ipv6-address, 160 | /// sin6_scope_id 161 | scope-id: u32, 162 | } 163 | 164 | @since(version = 0.2.0) 165 | variant ip-socket-address { 166 | ipv4(ipv4-socket-address), 167 | ipv6(ipv6-socket-address), 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /waki/wit/deps/sockets/tcp-create-socket.wit: -------------------------------------------------------------------------------- 1 | @since(version = 0.2.0) 2 | interface tcp-create-socket { 3 | @since(version = 0.2.0) 4 | use network.{network, error-code, ip-address-family}; 5 | @since(version = 0.2.0) 6 | use tcp.{tcp-socket}; 7 | 8 | /// Create a new TCP socket. 9 | /// 10 | /// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` in POSIX. 11 | /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. 12 | /// 13 | /// This function does not require a network capability handle. This is considered to be safe because 14 | /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind`/`connect` 15 | /// is called, the socket is effectively an in-memory configuration object, unable to communicate with the outside world. 16 | /// 17 | /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. 18 | /// 19 | /// # Typical errors 20 | /// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) 21 | /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) 22 | /// 23 | /// # References 24 | /// - 25 | /// - 26 | /// - 27 | /// - 28 | @since(version = 0.2.0) 29 | create-tcp-socket: func(address-family: ip-address-family) -> result; 30 | } 31 | -------------------------------------------------------------------------------- /waki/wit/deps/sockets/tcp.wit: -------------------------------------------------------------------------------- 1 | @since(version = 0.2.0) 2 | interface tcp { 3 | @since(version = 0.2.0) 4 | use wasi:io/streams@0.2.2.{input-stream, output-stream}; 5 | @since(version = 0.2.0) 6 | use wasi:io/poll@0.2.2.{pollable}; 7 | @since(version = 0.2.0) 8 | use wasi:clocks/monotonic-clock@0.2.2.{duration}; 9 | @since(version = 0.2.0) 10 | use network.{network, error-code, ip-socket-address, ip-address-family}; 11 | 12 | @since(version = 0.2.0) 13 | enum shutdown-type { 14 | /// Similar to `SHUT_RD` in POSIX. 15 | receive, 16 | 17 | /// Similar to `SHUT_WR` in POSIX. 18 | send, 19 | 20 | /// Similar to `SHUT_RDWR` in POSIX. 21 | both, 22 | } 23 | 24 | /// A TCP socket resource. 25 | /// 26 | /// The socket can be in one of the following states: 27 | /// - `unbound` 28 | /// - `bind-in-progress` 29 | /// - `bound` (See note below) 30 | /// - `listen-in-progress` 31 | /// - `listening` 32 | /// - `connect-in-progress` 33 | /// - `connected` 34 | /// - `closed` 35 | /// See 36 | /// for more information. 37 | /// 38 | /// Note: Except where explicitly mentioned, whenever this documentation uses 39 | /// the term "bound" without backticks it actually means: in the `bound` state *or higher*. 40 | /// (i.e. `bound`, `listen-in-progress`, `listening`, `connect-in-progress` or `connected`) 41 | /// 42 | /// In addition to the general error codes documented on the 43 | /// `network::error-code` type, TCP socket methods may always return 44 | /// `error(invalid-state)` when in the `closed` state. 45 | @since(version = 0.2.0) 46 | resource tcp-socket { 47 | /// Bind the socket to a specific network on the provided IP address and port. 48 | /// 49 | /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which 50 | /// network interface(s) to bind to. 51 | /// If the TCP/UDP port is zero, the socket will be bound to a random free port. 52 | /// 53 | /// Bind can be attempted multiple times on the same socket, even with 54 | /// different arguments on each iteration. But never concurrently and 55 | /// only as long as the previous bind failed. Once a bind succeeds, the 56 | /// binding can't be changed anymore. 57 | /// 58 | /// # Typical errors 59 | /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) 60 | /// - `invalid-argument`: `local-address` is not a unicast address. (EINVAL) 61 | /// - `invalid-argument`: `local-address` is an IPv4-mapped IPv6 address. (EINVAL) 62 | /// - `invalid-state`: The socket is already bound. (EINVAL) 63 | /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) 64 | /// - `address-in-use`: Address is already in use. (EADDRINUSE) 65 | /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) 66 | /// - `not-in-progress`: A `bind` operation is not in progress. 67 | /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) 68 | /// 69 | /// # Implementors note 70 | /// When binding to a non-zero port, this bind operation shouldn't be affected by the TIME_WAIT 71 | /// state of a recently closed socket on the same local address. In practice this means that the SO_REUSEADDR 72 | /// socket option should be set implicitly on all platforms, except on Windows where this is the default behavior 73 | /// and SO_REUSEADDR performs something different entirely. 74 | /// 75 | /// Unlike in POSIX, in WASI the bind operation is async. This enables 76 | /// interactive WASI hosts to inject permission prompts. Runtimes that 77 | /// don't want to make use of this ability can simply call the native 78 | /// `bind` as part of either `start-bind` or `finish-bind`. 79 | /// 80 | /// # References 81 | /// - 82 | /// - 83 | /// - 84 | /// - 85 | @since(version = 0.2.0) 86 | start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; 87 | @since(version = 0.2.0) 88 | finish-bind: func() -> result<_, error-code>; 89 | 90 | /// Connect to a remote endpoint. 91 | /// 92 | /// On success: 93 | /// - the socket is transitioned into the `connected` state. 94 | /// - a pair of streams is returned that can be used to read & write to the connection 95 | /// 96 | /// After a failed connection attempt, the socket will be in the `closed` 97 | /// state and the only valid action left is to `drop` the socket. A single 98 | /// socket can not be used to connect more than once. 99 | /// 100 | /// # Typical errors 101 | /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) 102 | /// - `invalid-argument`: `remote-address` is not a unicast address. (EINVAL, ENETUNREACH on Linux, EAFNOSUPPORT on MacOS) 103 | /// - `invalid-argument`: `remote-address` is an IPv4-mapped IPv6 address. (EINVAL, EADDRNOTAVAIL on Illumos) 104 | /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) 105 | /// - `invalid-argument`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) 106 | /// - `invalid-argument`: The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. 107 | /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN) 108 | /// - `invalid-state`: The socket is already in the `listening` state. (EOPNOTSUPP, EINVAL on Windows) 109 | /// - `timeout`: Connection timed out. (ETIMEDOUT) 110 | /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) 111 | /// - `connection-reset`: The connection was reset. (ECONNRESET) 112 | /// - `connection-aborted`: The connection was aborted. (ECONNABORTED) 113 | /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) 114 | /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) 115 | /// - `not-in-progress`: A connect operation is not in progress. 116 | /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) 117 | /// 118 | /// # Implementors note 119 | /// The POSIX equivalent of `start-connect` is the regular `connect` syscall. 120 | /// Because all WASI sockets are non-blocking this is expected to return 121 | /// EINPROGRESS, which should be translated to `ok()` in WASI. 122 | /// 123 | /// The POSIX equivalent of `finish-connect` is a `poll` for event `POLLOUT` 124 | /// with a timeout of 0 on the socket descriptor. Followed by a check for 125 | /// the `SO_ERROR` socket option, in case the poll signaled readiness. 126 | /// 127 | /// # References 128 | /// - 129 | /// - 130 | /// - 131 | /// - 132 | @since(version = 0.2.0) 133 | start-connect: func(network: borrow, remote-address: ip-socket-address) -> result<_, error-code>; 134 | @since(version = 0.2.0) 135 | finish-connect: func() -> result, error-code>; 136 | 137 | /// Start listening for new connections. 138 | /// 139 | /// Transitions the socket into the `listening` state. 140 | /// 141 | /// Unlike POSIX, the socket must already be explicitly bound. 142 | /// 143 | /// # Typical errors 144 | /// - `invalid-state`: The socket is not bound to any local address. (EDESTADDRREQ) 145 | /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN, EINVAL on BSD) 146 | /// - `invalid-state`: The socket is already in the `listening` state. 147 | /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) 148 | /// - `not-in-progress`: A listen operation is not in progress. 149 | /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) 150 | /// 151 | /// # Implementors note 152 | /// Unlike in POSIX, in WASI the listen operation is async. This enables 153 | /// interactive WASI hosts to inject permission prompts. Runtimes that 154 | /// don't want to make use of this ability can simply call the native 155 | /// `listen` as part of either `start-listen` or `finish-listen`. 156 | /// 157 | /// # References 158 | /// - 159 | /// - 160 | /// - 161 | /// - 162 | @since(version = 0.2.0) 163 | start-listen: func() -> result<_, error-code>; 164 | @since(version = 0.2.0) 165 | finish-listen: func() -> result<_, error-code>; 166 | 167 | /// Accept a new client socket. 168 | /// 169 | /// The returned socket is bound and in the `connected` state. The following properties are inherited from the listener socket: 170 | /// - `address-family` 171 | /// - `keep-alive-enabled` 172 | /// - `keep-alive-idle-time` 173 | /// - `keep-alive-interval` 174 | /// - `keep-alive-count` 175 | /// - `hop-limit` 176 | /// - `receive-buffer-size` 177 | /// - `send-buffer-size` 178 | /// 179 | /// On success, this function returns the newly accepted client socket along with 180 | /// a pair of streams that can be used to read & write to the connection. 181 | /// 182 | /// # Typical errors 183 | /// - `invalid-state`: Socket is not in the `listening` state. (EINVAL) 184 | /// - `would-block`: No pending connections at the moment. (EWOULDBLOCK, EAGAIN) 185 | /// - `connection-aborted`: An incoming connection was pending, but was terminated by the client before this listener could accept it. (ECONNABORTED) 186 | /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) 187 | /// 188 | /// # References 189 | /// - 190 | /// - 191 | /// - 192 | /// - 193 | @since(version = 0.2.0) 194 | accept: func() -> result, error-code>; 195 | 196 | /// Get the bound local address. 197 | /// 198 | /// POSIX mentions: 199 | /// > If the socket has not been bound to a local name, the value 200 | /// > stored in the object pointed to by `address` is unspecified. 201 | /// 202 | /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. 203 | /// 204 | /// # Typical errors 205 | /// - `invalid-state`: The socket is not bound to any local address. 206 | /// 207 | /// # References 208 | /// - 209 | /// - 210 | /// - 211 | /// - 212 | @since(version = 0.2.0) 213 | local-address: func() -> result; 214 | 215 | /// Get the remote address. 216 | /// 217 | /// # Typical errors 218 | /// - `invalid-state`: The socket is not connected to a remote address. (ENOTCONN) 219 | /// 220 | /// # References 221 | /// - 222 | /// - 223 | /// - 224 | /// - 225 | @since(version = 0.2.0) 226 | remote-address: func() -> result; 227 | 228 | /// Whether the socket is in the `listening` state. 229 | /// 230 | /// Equivalent to the SO_ACCEPTCONN socket option. 231 | @since(version = 0.2.0) 232 | is-listening: func() -> bool; 233 | 234 | /// Whether this is a IPv4 or IPv6 socket. 235 | /// 236 | /// Equivalent to the SO_DOMAIN socket option. 237 | @since(version = 0.2.0) 238 | address-family: func() -> ip-address-family; 239 | 240 | /// Hints the desired listen queue size. Implementations are free to ignore this. 241 | /// 242 | /// If the provided value is 0, an `invalid-argument` error is returned. 243 | /// Any other value will never cause an error, but it might be silently clamped and/or rounded. 244 | /// 245 | /// # Typical errors 246 | /// - `not-supported`: (set) The platform does not support changing the backlog size after the initial listen. 247 | /// - `invalid-argument`: (set) The provided value was 0. 248 | /// - `invalid-state`: (set) The socket is in the `connect-in-progress` or `connected` state. 249 | @since(version = 0.2.0) 250 | set-listen-backlog-size: func(value: u64) -> result<_, error-code>; 251 | 252 | /// Enables or disables keepalive. 253 | /// 254 | /// The keepalive behavior can be adjusted using: 255 | /// - `keep-alive-idle-time` 256 | /// - `keep-alive-interval` 257 | /// - `keep-alive-count` 258 | /// These properties can be configured while `keep-alive-enabled` is false, but only come into effect when `keep-alive-enabled` is true. 259 | /// 260 | /// Equivalent to the SO_KEEPALIVE socket option. 261 | @since(version = 0.2.0) 262 | keep-alive-enabled: func() -> result; 263 | @since(version = 0.2.0) 264 | set-keep-alive-enabled: func(value: bool) -> result<_, error-code>; 265 | 266 | /// Amount of time the connection has to be idle before TCP starts sending keepalive packets. 267 | /// 268 | /// If the provided value is 0, an `invalid-argument` error is returned. 269 | /// Any other value will never cause an error, but it might be silently clamped and/or rounded. 270 | /// I.e. after setting a value, reading the same setting back may return a different value. 271 | /// 272 | /// Equivalent to the TCP_KEEPIDLE socket option. (TCP_KEEPALIVE on MacOS) 273 | /// 274 | /// # Typical errors 275 | /// - `invalid-argument`: (set) The provided value was 0. 276 | @since(version = 0.2.0) 277 | keep-alive-idle-time: func() -> result; 278 | @since(version = 0.2.0) 279 | set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>; 280 | 281 | /// The time between keepalive packets. 282 | /// 283 | /// If the provided value is 0, an `invalid-argument` error is returned. 284 | /// Any other value will never cause an error, but it might be silently clamped and/or rounded. 285 | /// I.e. after setting a value, reading the same setting back may return a different value. 286 | /// 287 | /// Equivalent to the TCP_KEEPINTVL socket option. 288 | /// 289 | /// # Typical errors 290 | /// - `invalid-argument`: (set) The provided value was 0. 291 | @since(version = 0.2.0) 292 | keep-alive-interval: func() -> result; 293 | @since(version = 0.2.0) 294 | set-keep-alive-interval: func(value: duration) -> result<_, error-code>; 295 | 296 | /// The maximum amount of keepalive packets TCP should send before aborting the connection. 297 | /// 298 | /// If the provided value is 0, an `invalid-argument` error is returned. 299 | /// Any other value will never cause an error, but it might be silently clamped and/or rounded. 300 | /// I.e. after setting a value, reading the same setting back may return a different value. 301 | /// 302 | /// Equivalent to the TCP_KEEPCNT socket option. 303 | /// 304 | /// # Typical errors 305 | /// - `invalid-argument`: (set) The provided value was 0. 306 | @since(version = 0.2.0) 307 | keep-alive-count: func() -> result; 308 | @since(version = 0.2.0) 309 | set-keep-alive-count: func(value: u32) -> result<_, error-code>; 310 | 311 | /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. 312 | /// 313 | /// If the provided value is 0, an `invalid-argument` error is returned. 314 | /// 315 | /// # Typical errors 316 | /// - `invalid-argument`: (set) The TTL value must be 1 or higher. 317 | @since(version = 0.2.0) 318 | hop-limit: func() -> result; 319 | @since(version = 0.2.0) 320 | set-hop-limit: func(value: u8) -> result<_, error-code>; 321 | 322 | /// The kernel buffer space reserved for sends/receives on this socket. 323 | /// 324 | /// If the provided value is 0, an `invalid-argument` error is returned. 325 | /// Any other value will never cause an error, but it might be silently clamped and/or rounded. 326 | /// I.e. after setting a value, reading the same setting back may return a different value. 327 | /// 328 | /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. 329 | /// 330 | /// # Typical errors 331 | /// - `invalid-argument`: (set) The provided value was 0. 332 | @since(version = 0.2.0) 333 | receive-buffer-size: func() -> result; 334 | @since(version = 0.2.0) 335 | set-receive-buffer-size: func(value: u64) -> result<_, error-code>; 336 | @since(version = 0.2.0) 337 | send-buffer-size: func() -> result; 338 | @since(version = 0.2.0) 339 | set-send-buffer-size: func(value: u64) -> result<_, error-code>; 340 | 341 | /// Create a `pollable` which can be used to poll for, or block on, 342 | /// completion of any of the asynchronous operations of this socket. 343 | /// 344 | /// When `finish-bind`, `finish-listen`, `finish-connect` or `accept` 345 | /// return `error(would-block)`, this pollable can be used to wait for 346 | /// their success or failure, after which the method can be retried. 347 | /// 348 | /// The pollable is not limited to the async operation that happens to be 349 | /// in progress at the time of calling `subscribe` (if any). Theoretically, 350 | /// `subscribe` only has to be called once per socket and can then be 351 | /// (re)used for the remainder of the socket's lifetime. 352 | /// 353 | /// See 354 | /// for more information. 355 | /// 356 | /// Note: this function is here for WASI 0.2 only. 357 | /// It's planned to be removed when `future` is natively supported in Preview3. 358 | @since(version = 0.2.0) 359 | subscribe: func() -> pollable; 360 | 361 | /// Initiate a graceful shutdown. 362 | /// 363 | /// - `receive`: The socket is not expecting to receive any data from 364 | /// the peer. The `input-stream` associated with this socket will be 365 | /// closed. Any data still in the receive queue at time of calling 366 | /// this method will be discarded. 367 | /// - `send`: The socket has no more data to send to the peer. The `output-stream` 368 | /// associated with this socket will be closed and a FIN packet will be sent. 369 | /// - `both`: Same effect as `receive` & `send` combined. 370 | /// 371 | /// This function is idempotent; shutting down a direction more than once 372 | /// has no effect and returns `ok`. 373 | /// 374 | /// The shutdown function does not close (drop) the socket. 375 | /// 376 | /// # Typical errors 377 | /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) 378 | /// 379 | /// # References 380 | /// - 381 | /// - 382 | /// - 383 | /// - 384 | @since(version = 0.2.0) 385 | shutdown: func(shutdown-type: shutdown-type) -> result<_, error-code>; 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /waki/wit/deps/sockets/udp-create-socket.wit: -------------------------------------------------------------------------------- 1 | @since(version = 0.2.0) 2 | interface udp-create-socket { 3 | @since(version = 0.2.0) 4 | use network.{network, error-code, ip-address-family}; 5 | @since(version = 0.2.0) 6 | use udp.{udp-socket}; 7 | 8 | /// Create a new UDP socket. 9 | /// 10 | /// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` in POSIX. 11 | /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. 12 | /// 13 | /// This function does not require a network capability handle. This is considered to be safe because 14 | /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind` is called, 15 | /// the socket is effectively an in-memory configuration object, unable to communicate with the outside world. 16 | /// 17 | /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. 18 | /// 19 | /// # Typical errors 20 | /// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) 21 | /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) 22 | /// 23 | /// # References: 24 | /// - 25 | /// - 26 | /// - 27 | /// - 28 | @since(version = 0.2.0) 29 | create-udp-socket: func(address-family: ip-address-family) -> result; 30 | } 31 | -------------------------------------------------------------------------------- /waki/wit/deps/sockets/udp.wit: -------------------------------------------------------------------------------- 1 | @since(version = 0.2.0) 2 | interface udp { 3 | @since(version = 0.2.0) 4 | use wasi:io/poll@0.2.2.{pollable}; 5 | @since(version = 0.2.0) 6 | use network.{network, error-code, ip-socket-address, ip-address-family}; 7 | 8 | /// A received datagram. 9 | @since(version = 0.2.0) 10 | record incoming-datagram { 11 | /// The payload. 12 | /// 13 | /// Theoretical max size: ~64 KiB. In practice, typically less than 1500 bytes. 14 | data: list, 15 | 16 | /// The source address. 17 | /// 18 | /// This field is guaranteed to match the remote address the stream was initialized with, if any. 19 | /// 20 | /// Equivalent to the `src_addr` out parameter of `recvfrom`. 21 | remote-address: ip-socket-address, 22 | } 23 | 24 | /// A datagram to be sent out. 25 | @since(version = 0.2.0) 26 | record outgoing-datagram { 27 | /// The payload. 28 | data: list, 29 | 30 | /// The destination address. 31 | /// 32 | /// The requirements on this field depend on how the stream was initialized: 33 | /// - with a remote address: this field must be None or match the stream's remote address exactly. 34 | /// - without a remote address: this field is required. 35 | /// 36 | /// If this value is None, the send operation is equivalent to `send` in POSIX. Otherwise it is equivalent to `sendto`. 37 | remote-address: option, 38 | } 39 | 40 | /// A UDP socket handle. 41 | @since(version = 0.2.0) 42 | resource udp-socket { 43 | /// Bind the socket to a specific network on the provided IP address and port. 44 | /// 45 | /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which 46 | /// network interface(s) to bind to. 47 | /// If the port is zero, the socket will be bound to a random free port. 48 | /// 49 | /// # Typical errors 50 | /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) 51 | /// - `invalid-state`: The socket is already bound. (EINVAL) 52 | /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) 53 | /// - `address-in-use`: Address is already in use. (EADDRINUSE) 54 | /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) 55 | /// - `not-in-progress`: A `bind` operation is not in progress. 56 | /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) 57 | /// 58 | /// # Implementors note 59 | /// Unlike in POSIX, in WASI the bind operation is async. This enables 60 | /// interactive WASI hosts to inject permission prompts. Runtimes that 61 | /// don't want to make use of this ability can simply call the native 62 | /// `bind` as part of either `start-bind` or `finish-bind`. 63 | /// 64 | /// # References 65 | /// - 66 | /// - 67 | /// - 68 | /// - 69 | @since(version = 0.2.0) 70 | start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; 71 | @since(version = 0.2.0) 72 | finish-bind: func() -> result<_, error-code>; 73 | 74 | /// Set up inbound & outbound communication channels, optionally to a specific peer. 75 | /// 76 | /// This function only changes the local socket configuration and does not generate any network traffic. 77 | /// On success, the `remote-address` of the socket is updated. The `local-address` may be updated as well, 78 | /// based on the best network path to `remote-address`. 79 | /// 80 | /// When a `remote-address` is provided, the returned streams are limited to communicating with that specific peer: 81 | /// - `send` can only be used to send to this destination. 82 | /// - `receive` will only return datagrams sent from the provided `remote-address`. 83 | /// 84 | /// This method may be called multiple times on the same socket to change its association, but 85 | /// only the most recently returned pair of streams will be operational. Implementations may trap if 86 | /// the streams returned by a previous invocation haven't been dropped yet before calling `stream` again. 87 | /// 88 | /// The POSIX equivalent in pseudo-code is: 89 | /// ```text 90 | /// if (was previously connected) { 91 | /// connect(s, AF_UNSPEC) 92 | /// } 93 | /// if (remote_address is Some) { 94 | /// connect(s, remote_address) 95 | /// } 96 | /// ``` 97 | /// 98 | /// Unlike in POSIX, the socket must already be explicitly bound. 99 | /// 100 | /// # Typical errors 101 | /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) 102 | /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) 103 | /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) 104 | /// - `invalid-state`: The socket is not bound. 105 | /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) 106 | /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) 107 | /// - `connection-refused`: The connection was refused. (ECONNREFUSED) 108 | /// 109 | /// # References 110 | /// - 111 | /// - 112 | /// - 113 | /// - 114 | @since(version = 0.2.0) 115 | %stream: func(remote-address: option) -> result, error-code>; 116 | 117 | /// Get the current bound address. 118 | /// 119 | /// POSIX mentions: 120 | /// > If the socket has not been bound to a local name, the value 121 | /// > stored in the object pointed to by `address` is unspecified. 122 | /// 123 | /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. 124 | /// 125 | /// # Typical errors 126 | /// - `invalid-state`: The socket is not bound to any local address. 127 | /// 128 | /// # References 129 | /// - 130 | /// - 131 | /// - 132 | /// - 133 | @since(version = 0.2.0) 134 | local-address: func() -> result; 135 | 136 | /// Get the address the socket is currently streaming to. 137 | /// 138 | /// # Typical errors 139 | /// - `invalid-state`: The socket is not streaming to a specific remote address. (ENOTCONN) 140 | /// 141 | /// # References 142 | /// - 143 | /// - 144 | /// - 145 | /// - 146 | @since(version = 0.2.0) 147 | remote-address: func() -> result; 148 | 149 | /// Whether this is a IPv4 or IPv6 socket. 150 | /// 151 | /// Equivalent to the SO_DOMAIN socket option. 152 | @since(version = 0.2.0) 153 | address-family: func() -> ip-address-family; 154 | 155 | /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. 156 | /// 157 | /// If the provided value is 0, an `invalid-argument` error is returned. 158 | /// 159 | /// # Typical errors 160 | /// - `invalid-argument`: (set) The TTL value must be 1 or higher. 161 | @since(version = 0.2.0) 162 | unicast-hop-limit: func() -> result; 163 | @since(version = 0.2.0) 164 | set-unicast-hop-limit: func(value: u8) -> result<_, error-code>; 165 | 166 | /// The kernel buffer space reserved for sends/receives on this socket. 167 | /// 168 | /// If the provided value is 0, an `invalid-argument` error is returned. 169 | /// Any other value will never cause an error, but it might be silently clamped and/or rounded. 170 | /// I.e. after setting a value, reading the same setting back may return a different value. 171 | /// 172 | /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. 173 | /// 174 | /// # Typical errors 175 | /// - `invalid-argument`: (set) The provided value was 0. 176 | @since(version = 0.2.0) 177 | receive-buffer-size: func() -> result; 178 | @since(version = 0.2.0) 179 | set-receive-buffer-size: func(value: u64) -> result<_, error-code>; 180 | @since(version = 0.2.0) 181 | send-buffer-size: func() -> result; 182 | @since(version = 0.2.0) 183 | set-send-buffer-size: func(value: u64) -> result<_, error-code>; 184 | 185 | /// Create a `pollable` which will resolve once the socket is ready for I/O. 186 | /// 187 | /// Note: this function is here for WASI 0.2 only. 188 | /// It's planned to be removed when `future` is natively supported in Preview3. 189 | @since(version = 0.2.0) 190 | subscribe: func() -> pollable; 191 | } 192 | 193 | @since(version = 0.2.0) 194 | resource incoming-datagram-stream { 195 | /// Receive messages on the socket. 196 | /// 197 | /// This function attempts to receive up to `max-results` datagrams on the socket without blocking. 198 | /// The returned list may contain fewer elements than requested, but never more. 199 | /// 200 | /// This function returns successfully with an empty list when either: 201 | /// - `max-results` is 0, or: 202 | /// - `max-results` is greater than 0, but no results are immediately available. 203 | /// This function never returns `error(would-block)`. 204 | /// 205 | /// # Typical errors 206 | /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) 207 | /// - `connection-refused`: The connection was refused. (ECONNREFUSED) 208 | /// 209 | /// # References 210 | /// - 211 | /// - 212 | /// - 213 | /// - 214 | /// - 215 | /// - 216 | /// - 217 | /// - 218 | @since(version = 0.2.0) 219 | receive: func(max-results: u64) -> result, error-code>; 220 | 221 | /// Create a `pollable` which will resolve once the stream is ready to receive again. 222 | /// 223 | /// Note: this function is here for WASI 0.2 only. 224 | /// It's planned to be removed when `future` is natively supported in Preview3. 225 | @since(version = 0.2.0) 226 | subscribe: func() -> pollable; 227 | } 228 | 229 | @since(version = 0.2.0) 230 | resource outgoing-datagram-stream { 231 | /// Check readiness for sending. This function never blocks. 232 | /// 233 | /// Returns the number of datagrams permitted for the next call to `send`, 234 | /// or an error. Calling `send` with more datagrams than this function has 235 | /// permitted will trap. 236 | /// 237 | /// When this function returns ok(0), the `subscribe` pollable will 238 | /// become ready when this function will report at least ok(1), or an 239 | /// error. 240 | /// 241 | /// Never returns `would-block`. 242 | check-send: func() -> result; 243 | 244 | /// Send messages on the socket. 245 | /// 246 | /// This function attempts to send all provided `datagrams` on the socket without blocking and 247 | /// returns how many messages were actually sent (or queued for sending). This function never 248 | /// returns `error(would-block)`. If none of the datagrams were able to be sent, `ok(0)` is returned. 249 | /// 250 | /// This function semantically behaves the same as iterating the `datagrams` list and sequentially 251 | /// sending each individual datagram until either the end of the list has been reached or the first error occurred. 252 | /// If at least one datagram has been sent successfully, this function never returns an error. 253 | /// 254 | /// If the input list is empty, the function returns `ok(0)`. 255 | /// 256 | /// Each call to `send` must be permitted by a preceding `check-send`. Implementations must trap if 257 | /// either `check-send` was not called or `datagrams` contains more items than `check-send` permitted. 258 | /// 259 | /// # Typical errors 260 | /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) 261 | /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) 262 | /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) 263 | /// - `invalid-argument`: The socket is in "connected" mode and `remote-address` is `some` value that does not match the address passed to `stream`. (EISCONN) 264 | /// - `invalid-argument`: The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ) 265 | /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) 266 | /// - `connection-refused`: The connection was refused. (ECONNREFUSED) 267 | /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) 268 | /// 269 | /// # References 270 | /// - 271 | /// - 272 | /// - 273 | /// - 274 | /// - 275 | /// - 276 | /// - 277 | /// - 278 | @since(version = 0.2.0) 279 | send: func(datagrams: list) -> result; 280 | 281 | /// Create a `pollable` which will resolve once the stream is ready to send again. 282 | /// 283 | /// Note: this function is here for WASI 0.2 only. 284 | /// It's planned to be removed when `future` is natively supported in Preview3. 285 | @since(version = 0.2.0) 286 | subscribe: func() -> pollable; 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /waki/wit/deps/sockets/world.wit: -------------------------------------------------------------------------------- 1 | package wasi:sockets@0.2.2; 2 | 3 | @since(version = 0.2.0) 4 | world imports { 5 | @since(version = 0.2.0) 6 | import instance-network; 7 | @since(version = 0.2.0) 8 | import network; 9 | @since(version = 0.2.0) 10 | import udp; 11 | @since(version = 0.2.0) 12 | import udp-create-socket; 13 | @since(version = 0.2.0) 14 | import tcp; 15 | @since(version = 0.2.0) 16 | import tcp-create-socket; 17 | @since(version = 0.2.0) 18 | import ip-name-lookup; 19 | } 20 | -------------------------------------------------------------------------------- /waki/wit/world.wit: -------------------------------------------------------------------------------- 1 | package wacker-dev:waki; 2 | 3 | world http { 4 | include wasi:http/proxy@0.2.2; 5 | } 6 | --------------------------------------------------------------------------------