diff --git a/.gitignore b/.gitignore
index 575a1af..be13c22 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
.vscode/
-target/
\ No newline at end of file
+target/
+*.pyc
+*.pyo
diff --git a/samples/.gitignore b/samples/.gitignore
index 917660a..5145f80 100644
--- a/samples/.gitignore
+++ b/samples/.gitignore
@@ -1 +1,10 @@
-*.wasm
\ No newline at end of file
+# Ignore matching files
+poetry.lock
+*.wasm
+# Ignore any matching folders
+__pycache__/
+.venv/
+.env/
+# ignore generated bindings
+plugins/
+schemas/
\ No newline at end of file
diff --git a/samples/python/Makefile b/samples/python/Makefile
new file mode 100644
index 0000000..907ab4e
--- /dev/null
+++ b/samples/python/Makefile
@@ -0,0 +1,18 @@
+PLUGIN_FILE=my-plugin.wasm
+PLUGIN_FOLDER=myplugin
+
+schemas:
+ poetry run python3 myplugin/helpers/schemas.py
+
+bindings:
+ poetry run componentize-py --wit-path wit --world logcraft:lgc/plugins@0.1.0 bindings ${PLUGIN_FOLDER}
+
+build:
+ make clear
+ make schemas
+ poetry run componentize-py --wit-path wit --world logcraft:lgc/plugins@0.1.0 componentize -p ${PLUGIN_FOLDER} main -o ${PLUGIN_FILE}
+
+clear:
+ rm -f ${PLUGIN_FILE}
+ rm -rf ${PLUGIN_FOLDER}/plugins
+ rm -rf ${PLUGIN_FOLDER}/schemas
\ No newline at end of file
diff --git a/samples/python/README.md b/samples/python/README.md
index b29ab6d..0b15c67 100644
--- a/samples/python/README.md
+++ b/samples/python/README.md
@@ -1,6 +1,6 @@
# Python Plugin Sample
-This is an example plugin using python.
+This is an example plugin using Python.
---
**Documentation**: https://docs.logcraft.io
@@ -8,7 +8,7 @@ This is an example plugin using python.
## Poetry
-This example uses python poetry to manage dependencies but feel free to use your prefered package manager.
+This example uses Python poetry to manage dependencies but feel free to use your preferred package manager.
## Build
@@ -17,29 +17,33 @@ Building a plugin is a 2 steps process:
1. Build the bindings for your IDE. This is optional but advised for development.
```bash
-poetry run componentize-py --wit-path wit --world plugins bindings myplugin
-
+poetry run componentize-py --wit-path wit --world logcraft:lgc/plugins@0.1.0 bindings myplugin
```
-2. Build the plugin. This step automatically build the bindings regardless if you did it in the previous step or not. The resulting wasm file is the LogCraft CLI plugin.
+2. Build the plugin. This step automatically builds the bindings regardless if you did it in the previous step or not. The resulting wasm file is the LogCraft CLI plugin.
```bash
-poetry run componentize-py --wit-path wit --world plugin componentize -p myplugin main -o my-plugin.wasm
+poetry run componentize-py --wit-path wit --world logcraft:lgc/plugins@0.1.0 componentize -p myplugin main -o my-plugin.wasm
```
+Or, with the provided Makefile:
-## Important
+```bash
+% make bindings
+% make build
+% make clear
+```
-As of june 2024, `componentize-py` uses cpython runtime without `zlib`. This is an issue that has consequences: we cannot use python `requests` library and probably others.
+## Important
+As of June 2024, `componentize-py` uses cpython runtime without `zlib`. This is an issue that has consequences: we cannot use python `requests` library and probably others.
This is a known and identified problem that will be fixed in the future by the `componentize-py` team.
-To workaround this issue, we created a http library based on `sink`, this is available in this sample python application (`myplugin.client`).
-
+To workaround this issue, we created an HTTP library based on `sink`, which is available in this sample Python application (`myplugin.client`).
## WIT
-The wit files provides from:
+The wit files are provided from:
-1. `wit/world.wit` and `wit/plugin.wit` are LogCraft specific configuration files. These files define input/outputs of plugins.
+1. `wit/world.wit` and `wit/plugin.`wit` are LogCraft-specific configuration files. These files define the inputs/outputs of plugins.
2. `wit/deps/` come from [wasi-http](https://github.com/WebAssembly/wasi-http/tree/main/wit/deps)
\ No newline at end of file
diff --git a/samples/python/myplugin/client/__init__.py b/samples/python/myplugin/client/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/samples/python/myplugin/helpers/__init__.py b/samples/python/myplugin/helpers/__init__.py
new file mode 100644
index 0000000..254f221
--- /dev/null
+++ b/samples/python/myplugin/helpers/__init__.py
@@ -0,0 +1,2 @@
+# Copyright (c) 2023 LogCraft, SAS.
+# SPDX-License-Identifier: MPL-2.0
\ No newline at end of file
diff --git a/samples/python/myplugin/helpers/client/__init__.py b/samples/python/myplugin/helpers/client/__init__.py
new file mode 100644
index 0000000..254f221
--- /dev/null
+++ b/samples/python/myplugin/helpers/client/__init__.py
@@ -0,0 +1,2 @@
+# Copyright (c) 2023 LogCraft, SAS.
+# SPDX-License-Identifier: MPL-2.0
\ No newline at end of file
diff --git a/samples/python/myplugin/client/pool_loop.py b/samples/python/myplugin/helpers/client/pool_loop.py
similarity index 99%
rename from samples/python/myplugin/client/pool_loop.py
rename to samples/python/myplugin/helpers/client/pool_loop.py
index 4f075f3..b9e07ab 100644
--- a/samples/python/myplugin/client/pool_loop.py
+++ b/samples/python/myplugin/helpers/client/pool_loop.py
@@ -10,13 +10,13 @@
import asyncio
import socket
import subprocess
+from typing import Optional, cast
from plugins.types import Ok, Err
-from plugins.imports import types, streams, poll, outgoing_handler
+from plugins.imports import types, streams, poll
from plugins.imports.types import IncomingBody, OutgoingBody, OutgoingRequest, IncomingResponse
from plugins.imports.streams import StreamErrorClosed, InputStream
from plugins.imports.poll import Pollable
-from typing import Optional, cast
# Maximum number of bytes to read at a time
READ_SIZE: int = 16 * 1024
diff --git a/samples/python/myplugin/client/req.py b/samples/python/myplugin/helpers/client/req.py
similarity index 99%
rename from samples/python/myplugin/client/req.py
rename to samples/python/myplugin/helpers/client/req.py
index e40901a..1d7de79 100644
--- a/samples/python/myplugin/client/req.py
+++ b/samples/python/myplugin/helpers/client/req.py
@@ -4,6 +4,7 @@
import traceback
import poll_loop
from poll_loop import PollLoop, Sink, Stream
+
from plugins.types import Ok, Err
from plugins.imports.types import (
IncomingResponse, Method, Method_Get, Method_Head, Method_Post, Method_Put, Method_Delete, Method_Connect, Method_Options,
@@ -11,6 +12,7 @@
Fields, Scheme, Scheme_Http, Scheme_Https, Scheme_Other, OutgoingRequest, OutgoingBody
)
from plugins.imports.streams import StreamError_Closed
+
from dataclasses import dataclass
from collections.abc import MutableMapping
from typing import Optional
diff --git a/samples/python/myplugin/helpers/schemas.py b/samples/python/myplugin/helpers/schemas.py
new file mode 100644
index 0000000..3898760
--- /dev/null
+++ b/samples/python/myplugin/helpers/schemas.py
@@ -0,0 +1,32 @@
+# Copyright (c) 2023 LogCraft, SAS.
+# SPDX-License-Identifier: MPL-2.0
+import os
+
+def generate():
+ """
+ Convert .k files from the 'package' directory to .py files
+ """
+ root = os.path.join(os.path.dirname(__file__), '../')
+ root = os.path.abspath(root)
+
+ # schemas directory
+ schemas_dir = os.path.join(root, 'schemas')
+ if not os.path.exists(schemas_dir):
+ os.makedirs(schemas_dir)
+
+ # convert .k files to .py files
+ for filename in ["settings.k", "rule.k"]:
+ filepath = os.path.join(root, 'package', filename)
+ filepath = os.path.abspath(filepath)
+
+ with open(filepath, 'r') as f:
+ content = f.read()
+
+ variable = filename.replace('.k', '')
+ f_out = os.path.join(schemas_dir, filename.replace('.k', '.py'))
+ with open(f_out, 'w') as f:
+ f.write("%s = '''%s'''" % (variable, content))
+
+
+if __name__ == "__main__":
+ generate()
diff --git a/samples/python/myplugin/main.py b/samples/python/myplugin/main.py
index 0dbd9bc..e442815 100644
--- a/samples/python/myplugin/main.py
+++ b/samples/python/myplugin/main.py
@@ -1,68 +1,187 @@
# Copyright (c) 2023 LogCraft, SAS.
# SPDX-License-Identifier: MPL-2.0
-
+import json
from typing import Optional
# bindings generated by `componentize-py`
-from plugins import Plugins
-from plugins.exports.plugin import Metadata
from plugins.types import Err, Ok, Some, Result
+from plugins.exports import Plugin
+from plugins.exports.plugin import Metadata
+# bindings generated by `helpers.schemas.generate()`
+from schemas.settings import settings
+from schemas.rule import rule
+# NOTE:
# As of June 2024, the `requests` library is not supported due to missing dependencies
-# in the cpython runtime used by componentize-py (ssl support, zlib).
+# in the CPython runtime used by componentize-py (ssl support, zlib).
#
# A fix is planned for the future, so in the mean time, we use our own http library
# derivated from sink
# https://github.com/bytecodealliance/componentize-py/issues/96
+from helpers.client.req import Request, send
+
+class Plugin(Plugin):
+ """
+ The following functions are called by `lgc` and they receive these parameters, or a
+ combinaison of them:
+
+ `name`:
+ a string representing the name of the detection, this is what we have in the detection
+ file.
+
+ `config`:
+ a JSON string representing the service configuration. This is an dict representing the
+ service configuration. The available keys are defined in the `package/settings.k` file.
+
+ For example, in this sample code we are using a copy of the settings.k from the Splunk
+ plugin, so we should expect parameters such as 'endpoint', 'authorization', etc.
-from client.req import Request, Response, send
+ `params`:
+ similar to `config`, the `params` parameter represents the detection rule (i.e. the yaml
+ file). The available keys are defined in the `package/rule.k` file.
+ NOTE:
+ This code sample is minimalistic and does not implement the actual logic to create, read,
+ update or delete a detection. It only shows how to parse the JSON strings received from
+ the CLI and how to return the results. In a real plugin, consider implementing classes to
+ represent the settings.k and rule.k files.
+ """
-class Plugin(Plugins):
# func() -> metadata;
def load(self) -> Metadata:
"""
- The `load()` function is called when the plugin is installed using `lgc plugins install`.
+ The `load()` function is called when the plugin is installed using
+ `lgc plugins install /path/to/my-plugin.wasm`.
- It should return a `Metadata` object containing the plugin's name, version, author, and description.
- Make sure the name respect kebab-case (lowercase and separated by dashes). The provided information
- will be displayed in the lgc.yaml file.
+ It should return a `Metadata` object containing the plugin's name, version, author, and
+ description. Make sure the name respect kebab-case (lowercase and separated by dashes).
+
+ This information will be registered/displayed in the lgc.yaml file.
"""
return Metadata("my-plugin", "0.1.0", "LogCraft", "This is a famous plugin")
# func() -> string;
def settings(self) -> str:
- return "OK"
+ """
+ The `settings()` function is called by several `lgc` capabilities such as
+ `lgc validate` or `lgc services configure my-plugin`.
+ """
+ return settings
# func() -> string;
def schema(self) -> str:
- return "OK"
+ """
+ The `schema()` function is called by `lgc plugins schema my-plugin`.
+ """
+ return rule
# func(config: string, name: string, params: string) -> result, string>;
def create(self, config: str, name: str, params: str) -> Result[Optional[str], str]:
- return Ok(Some("create()"))
+ # decode the json string received from the CLI
+ try:
+ config = json.loads(config) # service configuration (settings.k)
+ params = json.loads(params) # detection rule (rule.k)
+ except Exception as e:
+ raise Err(str(e))
+
+ # Depending on the settings.k, we need to assemble an url that will be used to
+ # create the detection. Here we are using the `endpoint` key from the settings.k
+ # to create the url.
+ url = f"{config['endpoint']}/some/service/remote/path/{name}"
+ resp = send(Request("POST", url, {}, None))
+
+ if resp.status == 201:
+ # Rule content not needed
+ return ""
+ raise Err(str(resp.status))
# func(config: string, name: string, params: string) -> result , string>;
def read(self, config: str, name: str, params: str) -> Optional[str]:
- return Ok(Some("read()"))
+ # decode the json string received from the CLI
+ try:
+ config = json.loads(config) # service configuration (settings.k)
+ params = json.loads(params) # detection rule (rule.k)
+ except Exception as e:
+ raise Err(str(e))
+
+ # Depending on the settings.k, we need to assemble an url that will be used to
+ # retrieve a detection. Here we are using the `endpoint` key from the settings.k
+ # to create the url.
+ url = f"{config['endpoint']}/some/service/remote/path/{name}"
+ resp = send(Request("GET", url, {}, None))
+
+ if resp.status == 200:
+ # return a json/dict object as a string (representing the rule.k)
+ # ex: return json.dumps({"rule": "my-rule"})
+ return str(resp.body)
+ # If 404, detection does not exist and will be created
+ elif resp.status == 404:
+ return None
+ # For any other HTTP code return an error
+ else:
+ raise Err(f"Error: HTTP/{resp.status}")
# func(config: string, name: string, params: string) -> result , string>;
def update(self, config: str, name: str, params: str) -> Optional[str]:
- return Ok(Some("update()"))
+ # decode the json string received from the CLI
+ try:
+ config = json.loads(config) # service configuration (settings.k)
+ params = json.loads(params) # detection rule (rule.k)
+ except Exception as e:
+ raise Err(str(e))
+
+ # Depending on the settings.k, we need to assemble an url that will be used to
+ # retrieve a detection. Here we are using the `endpoint` key from the settings.k
+ # to create the url.
+ url = f"{config['endpoint']}/some/service/remote/path/{name}"
+ resp = send(Request("PUT", url, {}, None))
+
+ if resp.status == 200:
+ # Rule content not needed
+ return ""
+ raise Err(str(resp.status))
# func(config: string, name: string, params: string) -> result , string>;
def delete(self, config: str, name: str, params: str) -> Optional[str]:
- return Ok(Some("delete()"))
+ # decode the json string received from the CLI
+ try:
+ config = json.loads(config) # service configuration (settings.k)
+ params = json.loads(params) # detection rule (rule.k)
+ except Exception as e:
+ raise Err(str(e))
+
+ # Depending on the settings.k, we need to assemble an url that will be used to
+ # retrieve a detection. Here we are using the `endpoint` key from the settings.k
+ # to create the url.
+ url = f"{config['endpoint']}/some/service/remote/path/{name}"
+ resp = send(Request("DELETE", url, {}, None))
+
+ if resp.status == 201:
+ # Rule content not needed
+ return ""
+ elif resp.status == 404:
+ # Detection doesn't exists
+ return None
+ raise Err(str(resp.status))
# ping: func(config: string) -> result;
- def ping(self, config: str) -> int:
+ def ping(self, config: str) -> int:
+ """
+ `lgc services ping` will call this function to check if the service is up and running.
+
+ This is a sample implementation of the `ping` function that sends a GET request
+ to `https://google.fr` and returns the status code, or an error if the request
+ fails.
+ """
try:
- resp = send(Request("GET", "https://google.fr", {}, None))
+ # Service configuration
+ config = json.loads(config)
+ # Make GET request with service provided endpoint
+ resp = send(Request("GET", config["endpoint"], {}, None))
except Exception as e:
raise Err(str(e))
- if resp.status_code >= 400:
- raise Err(str(resp.status_code))
-
- return Ok(resp.status_code)
+ if resp.status >= 400:
+ raise Err(str(resp.status))
+ return resp.status
diff --git a/samples/python/myplugin/package/rule.k b/samples/python/myplugin/package/rule.k
new file mode 100644
index 0000000..2e5cf81
--- /dev/null
+++ b/samples/python/myplugin/package/rule.k
@@ -0,0 +1,75 @@
+# Copyright (c) 2023 LogCraft, SAS.
+# SPDX-License-Identifier: MPL-2.0
+
+schema Rule:
+ """Splunk Detection Rule
+
+ Attributes
+ ----------
+ app: str, required
+ Application name
+ """
+ app: str
+ savedsearch: SavedSearch
+
+schema SavedSearch:
+ """Splunk Saved Search
+ cron_schedule: str, optional
+ Valid values: cron string.
+ disabled: bool, optional
+ Indicates if the saved search is enabled. Defaults to false.
+ dispatch_buckets: int, optional
+ The maximum number of timeline buckets. Defaults to 0.
+ display_view: str, optional
+ Defines the default UI view name (not label) in which to load the results.
+ is_scheduled: bool, optional
+ Whether this search is to be run on a schedule.
+ is_visible: bool, optional
+ Specifies whether this saved search should be listed in the visible saved search list. Defaults to true.
+ max_concurrent: int, optional
+ The maximum number of concurrent instances of this search the scheduler is allowed to run. Defaults to 1.
+ realtime_schedule: bool, optional
+ Controls the way the scheduler computes the next execution time of a scheduled search.
+ request_ui_dispatch_app: str, optional
+ Specifies a field used by Splunk Web to denote the app this search should be dispatched in.
+ request_ui_dispatch_view: str, optional
+ Specifies a field used by Splunk Web to denote the view this search should be displayed in.
+ restart_on_searchpeer_add: bool, optional
+ Specifies whether to restart a real-time search managed by the scheduler when a search peer becomes available for this saved search. Defaults to true.
+ run_on_startup: bool, optional
+ Specifies whether to restart a real-time search managed by the scheduler when a search peer becomes available for this saved search. Defaults to true.
+ schedule_window: str, optional
+ Time window (in minutes) during which the search has lower priority. Defaults to 0.
+ schedule_priority: str, optional
+ Raises the scheduling priority of the named search.
+ search: str, optional
+ Required when creating a new search.
+ vsid: str, optional
+ Defines the viewstate id associated with the UI view listed in 'displayview'.
+ workload_pool: str, optional
+ Specifies the new workload pool where the existing running search will be placed.
+ action: any, optional
+ Enable or disable an alert action.
+ dispatch: any, optional
+ Wildcard argument that accepts any dispatch related argument.
+ """
+ cron_schedule?: str
+ description?: str
+ disabled?: bool
+ dispatch_buckets?: int
+ display_view?: str
+ is_scheduled?: bool
+ is_visible?: bool
+ max_concurrent?: int
+ realtime_schedule?: bool
+ request_ui_dispatch_app?: str
+ request_ui_dispatch_view?: str
+ restart_on_searchpeer_add?: bool
+ run_on_startup?: bool
+ schedule_window?: str
+ schedule_priority?: str
+ search?: str
+ vsid?: str
+ workload_pool?: str
+ action?: any
+ dispatch?: any
\ No newline at end of file
diff --git a/samples/python/myplugin/package/settings.k b/samples/python/myplugin/package/settings.k
new file mode 100644
index 0000000..6c66d83
--- /dev/null
+++ b/samples/python/myplugin/package/settings.k
@@ -0,0 +1,35 @@
+# Copyright (c) 2023 LogCraft, SAS.
+# SPDX-License-Identifier: MPL-2.0
+
+import regex
+
+schema Configuration:
+ """Splunk Configuration
+
+ Attributes
+ ----------
+ endpoint: str, required
+ Splunk URL
+ authorization_scheme: str, required
+ Authorization scheme
+ authorization: str, required
+ Authorization
+ timeout: int, optional
+ Timeout in seconds
+ """
+ # Mandatory Parameters
+
+ # Splunk URL
+ endpoint: str = 'https://splunk_endpoint:8089'
+ # Authorization scheme
+ authorization_scheme: AuthorizationScheme = "Bearer"
+ # Authorization
+ @info(sensitive="true")
+ authorization: str = "base64_encoded_token"
+ # Timeout
+ timeout?: int = 60
+
+ check:
+ regex.match(endpoint, "^https?://"), "Incorrect endpoint, must start with http:// or https://."
+
+type AuthorizationScheme = "Bearer" | "Basic"
\ No newline at end of file
diff --git a/samples/python/poetry.lock b/samples/python/poetry.lock
deleted file mode 100644
index 4efe417..0000000
--- a/samples/python/poetry.lock
+++ /dev/null
@@ -1,21 +0,0 @@
-# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
-
-[[package]]
-name = "componentize-py"
-version = "0.13.5"
-description = "Tool to package Python applications as WebAssembly components"
-optional = false
-python-versions = "*"
-files = [
- {file = "componentize_py-0.13.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:1914553c3093a8b16d056f81a7932db0cb874842cb032a2be68f880f9621cec9"},
- {file = "componentize_py-0.13.5-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:0cdf5bfb17899e56cb6d27f84495ad3ba10fc2011debcf50c9f0cb4210f2d970"},
- {file = "componentize_py-0.13.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7fa9c80a390085db37e03adf90a8b9e04f37e9b083b2a131fe97da057ccdbed"},
- {file = "componentize_py-0.13.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b3cf7390453ffa8b45d8266ec11b7afa4f16503a6a43014fa5fa2e2d8dd3b76e"},
- {file = "componentize_py-0.13.5-cp37-abi3-win_amd64.whl", hash = "sha256:dbc8ac466d1a1074709d3344c03861507dacabab964b8ff13da59d0d363f166f"},
- {file = "componentize_py-0.13.5.tar.gz", hash = "sha256:1a41d92f3a8ff0f4b20894b9d0b0c0015fe3c77bedba1bf7bafe1257d177688a"},
-]
-
-[metadata]
-lock-version = "2.0"
-python-versions = "^3.12"
-content-hash = "dd30b9ee63591d0a34952ddd2d2c35037ae1c37a011900fbdfe7a5246ec108cb"
diff --git a/samples/python/pyproject.toml b/samples/python/pyproject.toml
index 66c76c3..fbfd736 100644
--- a/samples/python/pyproject.toml
+++ b/samples/python/pyproject.toml
@@ -1,3 +1,6 @@
+# Copyright (c) 2023 LogCraft, SAS.
+# SPDX-License-Identifier: MPL-2.0
+
[tool.poetry]
name = "myplugin"
version = "0.1.0"
@@ -8,6 +11,11 @@ readme = "README.md"
[tool.poetry.dependencies]
python = "^3.12"
componentize-py = "^0.13.5"
+asyncio = "^3.4.3"
+mypy = "^1.10.1"
+
+[tool.poetry.group.test.dependencies]
+ruff = "^0.5.0"
[build-system]
requires = ["poetry-core"]
diff --git a/samples/python/wit/deps/http/handler.wit b/samples/python/wit/deps/http/handler.wit
new file mode 100644
index 0000000..a34a064
--- /dev/null
+++ b/samples/python/wit/deps/http/handler.wit
@@ -0,0 +1,43 @@
+/// This interface defines a handler of incoming HTTP Requests. It should
+/// be exported by components which can respond to HTTP Requests.
+interface incoming-handler {
+ use types.{incoming-request, response-outparam};
+
+ /// This function is invoked with an incoming HTTP Request, and a resource
+ /// `response-outparam` which provides the capability to reply with an HTTP
+ /// Response. The response is sent by calling the `response-outparam.set`
+ /// method, which allows execution to continue after the response has been
+ /// sent. This enables both streaming to the response body, and performing other
+ /// work.
+ ///
+ /// The implementor of this function must write a response to the
+ /// `response-outparam` before returning, or else the caller will respond
+ /// with an error on its behalf.
+ handle: func(
+ request: incoming-request,
+ response-out: response-outparam
+ );
+}
+
+/// This interface defines a handler of outgoing HTTP Requests. It should be
+/// imported by components which wish to make HTTP Requests.
+interface outgoing-handler {
+ use types.{
+ outgoing-request, request-options, future-incoming-response, error-code
+ };
+
+ /// This function is invoked with an outgoing HTTP Request, and it returns
+ /// a resource `future-incoming-response` which represents an HTTP Response
+ /// which may arrive in the future.
+ ///
+ /// The `options` argument accepts optional parameters for the HTTP
+ /// protocol's transport layer.
+ ///
+ /// This function may return an error if the `outgoing-request` is invalid
+ /// or not allowed to be made. Otherwise, protocol errors are reported
+ /// through the `future-incoming-response`.
+ handle: func(
+ request: outgoing-request,
+ options: option
+ ) -> result;
+}
diff --git a/samples/python/wit/deps/http/proxy.wit b/samples/python/wit/deps/http/proxy.wit
new file mode 100644
index 0000000..687c24d
--- /dev/null
+++ b/samples/python/wit/deps/http/proxy.wit
@@ -0,0 +1,32 @@
+package wasi:http@0.2.0;
+
+/// The `wasi:http/proxy` world captures a widely-implementable intersection of
+/// hosts that includes HTTP forward and reverse proxies. Components targeting
+/// this world may concurrently stream in and out any number of incoming and
+/// outgoing HTTP requests.
+world proxy {
+ /// HTTP proxies have access to time and randomness.
+ include wasi:clocks/imports@0.2.0;
+ import wasi:random/random@0.2.0;
+
+ /// Proxies have standard output and error streams which are expected to
+ /// terminate in a developer-facing console provided by the host.
+ import wasi:cli/stdout@0.2.0;
+ import wasi:cli/stderr@0.2.0;
+
+ /// TODO: this is a temporary workaround until component tooling is able to
+ /// gracefully handle the absence of stdin. Hosts must return an eof stream
+ /// for this import, which is what wasi-libc + tooling will do automatically
+ /// when this import is properly removed.
+ import wasi:cli/stdin@0.2.0;
+
+ /// This is the default handler to use when user code simply wants to make an
+ /// HTTP request (e.g., via `fetch()`).
+ import outgoing-handler;
+
+ /// The host delivers incoming HTTP requests to a component by calling the
+ /// `handle` function of this exported interface. A host may arbitrarily reuse
+ /// or not reuse component instance when delivering incoming HTTP requests and
+ /// thus a component must be able to handle 0..N calls to `handle`.
+ export incoming-handler;
+}
diff --git a/samples/python/wit/deps/http/types.wit b/samples/python/wit/deps/http/types.wit
new file mode 100644
index 0000000..755ac6a
--- /dev/null
+++ b/samples/python/wit/deps/http/types.wit
@@ -0,0 +1,570 @@
+/// This interface defines all of the types and methods for implementing
+/// HTTP Requests and Responses, both incoming and outgoing, as well as
+/// their headers, trailers, and bodies.
+interface types {
+ use wasi:clocks/monotonic-clock@0.2.0.{duration};
+ use wasi:io/streams@0.2.0.{input-stream, output-stream};
+ use wasi:io/error@0.2.0.{error as io-error};
+ use wasi:io/poll@0.2.0.{pollable};
+
+ /// This type corresponds to HTTP standard Methods.
+ variant method {
+ get,
+ head,
+ post,
+ put,
+ delete,
+ connect,
+ options,
+ trace,
+ patch,
+ other(string)
+ }
+
+ /// This type corresponds to HTTP standard Related Schemes.
+ variant scheme {
+ HTTP,
+ HTTPS,
+ other(string)
+ }
+
+ /// These cases are inspired by the IANA HTTP Proxy Error Types:
+ /// https://www.iana.org/assignments/http-proxy-status/http-proxy-status.xhtml#table-http-proxy-error-types
+ variant error-code {
+ DNS-timeout,
+ DNS-error(DNS-error-payload),
+ destination-not-found,
+ destination-unavailable,
+ destination-IP-prohibited,
+ destination-IP-unroutable,
+ connection-refused,
+ connection-terminated,
+ connection-timeout,
+ connection-read-timeout,
+ connection-write-timeout,
+ connection-limit-reached,
+ TLS-protocol-error,
+ TLS-certificate-error,
+ TLS-alert-received(TLS-alert-received-payload),
+ HTTP-request-denied,
+ HTTP-request-length-required,
+ HTTP-request-body-size(option),
+ HTTP-request-method-invalid,
+ HTTP-request-URI-invalid,
+ HTTP-request-URI-too-long,
+ HTTP-request-header-section-size(option),
+ HTTP-request-header-size(option),
+ HTTP-request-trailer-section-size(option),
+ HTTP-request-trailer-size(field-size-payload),
+ HTTP-response-incomplete,
+ HTTP-response-header-section-size(option),
+ HTTP-response-header-size(field-size-payload),
+ HTTP-response-body-size(option),
+ HTTP-response-trailer-section-size(option),
+ HTTP-response-trailer-size(field-size-payload),
+ HTTP-response-transfer-coding(option),
+ HTTP-response-content-coding(option),
+ HTTP-response-timeout,
+ HTTP-upgrade-failed,
+ HTTP-protocol-error,
+ loop-detected,
+ configuration-error,
+ /// This is a catch-all error for anything that doesn't fit cleanly into a
+ /// more specific case. It also includes an optional string for an
+ /// unstructured description of the error. Users should not depend on the
+ /// string for diagnosing errors, as it's not required to be consistent
+ /// between implementations.
+ internal-error(option)
+ }
+
+ /// Defines the case payload type for `DNS-error` above:
+ record DNS-error-payload {
+ rcode: option,
+ info-code: option
+ }
+
+ /// Defines the case payload type for `TLS-alert-received` above:
+ record TLS-alert-received-payload {
+ alert-id: option,
+ alert-message: option
+ }
+
+ /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above:
+ record field-size-payload {
+ field-name: option,
+ field-size: option
+ }
+
+ /// Attempts to extract a http-related `error` from the wasi:io `error`
+ /// provided.
+ ///
+ /// Stream operations which return
+ /// `wasi:io/stream/stream-error::last-operation-failed` have a payload of
+ /// type `wasi:io/error/error` with more information about the operation
+ /// that failed. This payload can be passed through to this function to see
+ /// if there's http-related information about the error to return.
+ ///
+ /// Note that this function is fallible because not all io-errors are
+ /// http-related errors.
+ http-error-code: func(err: borrow) -> option;
+
+ /// This type enumerates the different kinds of errors that may occur when
+ /// setting or appending to a `fields` resource.
+ variant header-error {
+ /// This error indicates that a `field-key` or `field-value` was
+ /// syntactically invalid when used with an operation that sets headers in a
+ /// `fields`.
+ invalid-syntax,
+
+ /// This error indicates that a forbidden `field-key` was used when trying
+ /// to set a header in a `fields`.
+ forbidden,
+
+ /// This error indicates that the operation on the `fields` was not
+ /// permitted because the fields are immutable.
+ immutable,
+ }
+
+ /// Field keys are always strings.
+ type field-key = string;
+
+ /// Field values should always be ASCII strings. However, in
+ /// reality, HTTP implementations often have to interpret malformed values,
+ /// so they are provided as a list of bytes.
+ type field-value = list;
+
+ /// This following block defines the `fields` resource which corresponds to
+ /// HTTP standard Fields. Fields are a common representation used for both
+ /// Headers and Trailers.
+ ///
+ /// A `fields` may be mutable or immutable. A `fields` created using the
+ /// constructor, `from-list`, or `clone` will be mutable, but a `fields`
+ /// resource given by other means (including, but not limited to,
+ /// `incoming-request.headers`, `outgoing-request.headers`) might be be
+ /// immutable. In an immutable fields, the `set`, `append`, and `delete`
+ /// operations will fail with `header-error.immutable`.
+ resource fields {
+
+ /// Construct an empty HTTP Fields.
+ ///
+ /// The resulting `fields` is mutable.
+ constructor();
+
+ /// Construct an HTTP Fields.
+ ///
+ /// The resulting `fields` is mutable.
+ ///
+ /// The list represents each key-value pair in the Fields. Keys
+ /// which have multiple values are represented by multiple entries in this
+ /// list with the same key.
+ ///
+ /// The tuple is a pair of the field key, represented as a string, and
+ /// Value, represented as a list of bytes. In a valid Fields, all keys
+ /// and values are valid UTF-8 strings. However, values are not always
+ /// well-formed, so they are represented as a raw list of bytes.
+ ///
+ /// An error result will be returned if any header or value was
+ /// syntactically invalid, or if a header was forbidden.
+ from-list: static func(
+ entries: list>
+ ) -> result;
+
+ /// Get all of the values corresponding to a key. If the key is not present
+ /// in this `fields`, an empty list is returned. However, if the key is
+ /// present but empty, this is represented by a list with one or more
+ /// empty field-values present.
+ get: func(name: field-key) -> list;
+
+ /// Returns `true` when the key is present in this `fields`. If the key is
+ /// syntactically invalid, `false` is returned.
+ has: func(name: field-key) -> bool;
+
+ /// Set all of the values for a key. Clears any existing values for that
+ /// key, if they have been set.
+ ///
+ /// Fails with `header-error.immutable` if the `fields` are immutable.
+ set: func(name: field-key, value: list) -> result<_, header-error>;
+
+ /// Delete all values for a key. Does nothing if no values for the key
+ /// exist.
+ ///
+ /// Fails with `header-error.immutable` if the `fields` are immutable.
+ delete: func(name: field-key) -> result<_, header-error>;
+
+ /// Append a value for a key. Does not change or delete any existing
+ /// values for that key.
+ ///
+ /// Fails with `header-error.immutable` if the `fields` are immutable.
+ append: func(name: field-key, value: field-value) -> result<_, header-error>;
+
+ /// Retrieve the full set of keys and values in the Fields. Like the
+ /// constructor, the list represents each key-value pair.
+ ///
+ /// The outer list represents each key-value pair in the Fields. Keys
+ /// which have multiple values are represented by multiple entries in this
+ /// list with the same key.
+ entries: func() -> list>;
+
+ /// Make a deep copy of the Fields. Equivelant in behavior to calling the
+ /// `fields` constructor on the return value of `entries`. The resulting
+ /// `fields` is mutable.
+ clone: func() -> fields;
+ }
+
+ /// Headers is an alias for Fields.
+ type headers = fields;
+
+ /// Trailers is an alias for Fields.
+ type trailers = fields;
+
+ /// Represents an incoming HTTP Request.
+ resource incoming-request {
+
+ /// Returns the method of the incoming request.
+ method: func() -> method;
+
+ /// Returns the path with query parameters from the request, as a string.
+ path-with-query: func() -> option;
+
+ /// Returns the protocol scheme from the request.
+ scheme: func() -> option;
+
+ /// Returns the authority from the request, if it was present.
+ authority: func() -> option;
+
+ /// Get the `headers` associated with the request.
+ ///
+ /// The returned `headers` resource is immutable: `set`, `append`, and
+ /// `delete` operations will fail with `header-error.immutable`.
+ ///
+ /// The `headers` returned are a child resource: it must be dropped before
+ /// the parent `incoming-request` is dropped. Dropping this
+ /// `incoming-request` before all children are dropped will trap.
+ headers: func() -> headers;
+
+ /// Gives the `incoming-body` associated with this request. Will only
+ /// return success at most once, and subsequent calls will return error.
+ consume: func() -> result;
+ }
+
+ /// Represents an outgoing HTTP Request.
+ resource outgoing-request {
+
+ /// Construct a new `outgoing-request` with a default `method` of `GET`, and
+ /// `none` values for `path-with-query`, `scheme`, and `authority`.
+ ///
+ /// * `headers` is the HTTP Headers for the Request.
+ ///
+ /// It is possible to construct, or manipulate with the accessor functions
+ /// below, an `outgoing-request` with an invalid combination of `scheme`
+ /// and `authority`, or `headers` which are not permitted to be sent.
+ /// It is the obligation of the `outgoing-handler.handle` implementation
+ /// to reject invalid constructions of `outgoing-request`.
+ constructor(
+ headers: headers
+ );
+
+ /// Returns the resource corresponding to the outgoing Body for this
+ /// Request.
+ ///
+ /// Returns success on the first call: the `outgoing-body` resource for
+ /// this `outgoing-request` can be retrieved at most once. Subsequent
+ /// calls will return error.
+ body: func() -> result;
+
+ /// Get the Method for the Request.
+ method: func() -> method;
+ /// Set the Method for the Request. Fails if the string present in a
+ /// `method.other` argument is not a syntactically valid method.
+ set-method: func(method: method) -> result;
+
+ /// Get the combination of the HTTP Path and Query for the Request.
+ /// When `none`, this represents an empty Path and empty Query.
+ path-with-query: func() -> option;
+ /// Set the combination of the HTTP Path and Query for the Request.
+ /// When `none`, this represents an empty Path and empty Query. Fails is the
+ /// string given is not a syntactically valid path and query uri component.
+ set-path-with-query: func(path-with-query: option) -> result;
+
+ /// Get the HTTP Related Scheme for the Request. When `none`, the
+ /// implementation may choose an appropriate default scheme.
+ scheme: func() -> option;
+ /// Set the HTTP Related Scheme for the Request. When `none`, the
+ /// implementation may choose an appropriate default scheme. Fails if the
+ /// string given is not a syntactically valid uri scheme.
+ set-scheme: func(scheme: option) -> result;
+
+ /// Get the HTTP Authority for the Request. A value of `none` may be used
+ /// with Related Schemes which do not require an Authority. The HTTP and
+ /// HTTPS schemes always require an authority.
+ authority: func() -> option;
+ /// Set the HTTP Authority for the Request. A value of `none` may be used
+ /// with Related Schemes which do not require an Authority. The HTTP and
+ /// HTTPS schemes always require an authority. Fails if the string given is
+ /// not a syntactically valid uri authority.
+ set-authority: func(authority: option) -> result;
+
+ /// Get the headers associated with the Request.
+ ///
+ /// The returned `headers` resource is immutable: `set`, `append`, and
+ /// `delete` operations will fail with `header-error.immutable`.
+ ///
+ /// This headers resource is a child: it must be dropped before the parent
+ /// `outgoing-request` is dropped, or its ownership is transfered to
+ /// another component by e.g. `outgoing-handler.handle`.
+ headers: func() -> headers;
+ }
+
+ /// Parameters for making an HTTP Request. Each of these parameters is
+ /// currently an optional timeout applicable to the transport layer of the
+ /// HTTP protocol.
+ ///
+ /// These timeouts are separate from any the user may use to bound a
+ /// blocking call to `wasi:io/poll.poll`.
+ resource request-options {
+ /// Construct a default `request-options` value.
+ constructor();
+
+ /// The timeout for the initial connect to the HTTP Server.
+ connect-timeout: func() -> option;
+
+ /// Set the timeout for the initial connect to the HTTP Server. An error
+ /// return value indicates that this timeout is not supported.
+ set-connect-timeout: func(duration: option) -> result;
+
+ /// The timeout for receiving the first byte of the Response body.
+ first-byte-timeout: func() -> option;
+
+ /// Set the timeout for receiving the first byte of the Response body. An
+ /// error return value indicates that this timeout is not supported.
+ set-first-byte-timeout: func(duration: option) -> result;
+
+ /// The timeout for receiving subsequent chunks of bytes in the Response
+ /// body stream.
+ between-bytes-timeout: func() -> option;
+
+ /// Set the timeout for receiving subsequent chunks of bytes in the Response
+ /// body stream. An error return value indicates that this timeout is not
+ /// supported.
+ set-between-bytes-timeout: func(duration: option) -> result;
+ }
+
+ /// Represents the ability to send an HTTP Response.
+ ///
+ /// This resource is used by the `wasi:http/incoming-handler` interface to
+ /// allow a Response to be sent corresponding to the Request provided as the
+ /// other argument to `incoming-handler.handle`.
+ resource response-outparam {
+
+ /// Set the value of the `response-outparam` to either send a response,
+ /// or indicate an error.
+ ///
+ /// This method consumes the `response-outparam` to ensure that it is
+ /// called at most once. If it is never called, the implementation
+ /// will respond with an error.
+ ///
+ /// The user may provide an `error` to `response` to allow the
+ /// implementation determine how to respond with an HTTP error response.
+ set: static func(
+ param: response-outparam,
+ response: result,
+ );
+ }
+
+ /// This type corresponds to the HTTP standard Status Code.
+ type status-code = u16;
+
+ /// Represents an incoming HTTP Response.
+ resource incoming-response {
+
+ /// Returns the status code from the incoming response.
+ status: func() -> status-code;
+
+ /// Returns the headers from the incoming response.
+ ///
+ /// The returned `headers` resource is immutable: `set`, `append`, and
+ /// `delete` operations will fail with `header-error.immutable`.
+ ///
+ /// This headers resource is a child: it must be dropped before the parent
+ /// `incoming-response` is dropped.
+ headers: func() -> headers;
+
+ /// Returns the incoming body. May be called at most once. Returns error
+ /// if called additional times.
+ consume: func() -> result;
+ }
+
+ /// Represents an incoming HTTP Request or Response's Body.
+ ///
+ /// A body has both its contents - a stream of bytes - and a (possibly
+ /// empty) set of trailers, indicating that the full contents of the
+ /// body have been received. This resource represents the contents as
+ /// an `input-stream` and the delivery of trailers as a `future-trailers`,
+ /// and ensures that the user of this interface may only be consuming either
+ /// the body contents or waiting on trailers at any given time.
+ resource incoming-body {
+
+ /// Returns the contents of the body, as a stream of bytes.
+ ///
+ /// Returns success on first call: the stream representing the contents
+ /// can be retrieved at most once. Subsequent calls will return error.
+ ///
+ /// The returned `input-stream` resource is a child: it must be dropped
+ /// before the parent `incoming-body` is dropped, or consumed by
+ /// `incoming-body.finish`.
+ ///
+ /// This invariant ensures that the implementation can determine whether
+ /// the user is consuming the contents of the body, waiting on the
+ /// `future-trailers` to be ready, or neither. This allows for network
+ /// backpressure is to be applied when the user is consuming the body,
+ /// and for that backpressure to not inhibit delivery of the trailers if
+ /// the user does not read the entire body.
+ %stream: func() -> result;
+
+ /// Takes ownership of `incoming-body`, and returns a `future-trailers`.
+ /// This function will trap if the `input-stream` child is still alive.
+ finish: static func(this: incoming-body) -> future-trailers;
+ }
+
+ /// Represents a future which may eventaully return trailers, or an error.
+ ///
+ /// In the case that the incoming HTTP Request or Response did not have any
+ /// trailers, this future will resolve to the empty set of trailers once the
+ /// complete Request or Response body has been received.
+ resource future-trailers {
+
+ /// Returns a pollable which becomes ready when either the trailers have
+ /// been received, or an error has occured. When this pollable is ready,
+ /// the `get` method will return `some`.
+ subscribe: func() -> pollable;
+
+ /// Returns the contents of the trailers, or an error which occured,
+ /// once the future is ready.
+ ///
+ /// The outer `option` represents future readiness. Users can wait on this
+ /// `option` to become `some` using the `subscribe` method.
+ ///
+ /// The outer `result` is used to retrieve the trailers or error at most
+ /// once. It will be success on the first call in which the outer option
+ /// is `some`, and error on subsequent calls.
+ ///
+ /// The inner `result` represents that either the HTTP Request or Response
+ /// body, as well as any trailers, were received successfully, or that an
+ /// error occured receiving them. The optional `trailers` indicates whether
+ /// or not trailers were present in the body.
+ ///
+ /// When some `trailers` are returned by this method, the `trailers`
+ /// resource is immutable, and a child. Use of the `set`, `append`, or
+ /// `delete` methods will return an error, and the resource must be
+ /// dropped before the parent `future-trailers` is dropped.
+ get: func() -> option, error-code>>>;
+ }
+
+ /// Represents an outgoing HTTP Response.
+ resource outgoing-response {
+
+ /// Construct an `outgoing-response`, with a default `status-code` of `200`.
+ /// If a different `status-code` is needed, it must be set via the
+ /// `set-status-code` method.
+ ///
+ /// * `headers` is the HTTP Headers for the Response.
+ constructor(headers: headers);
+
+ /// Get the HTTP Status Code for the Response.
+ status-code: func() -> status-code;
+
+ /// Set the HTTP Status Code for the Response. Fails if the status-code
+ /// given is not a valid http status code.
+ set-status-code: func(status-code: status-code) -> result;
+
+ /// Get the headers associated with the Request.
+ ///
+ /// The returned `headers` resource is immutable: `set`, `append`, and
+ /// `delete` operations will fail with `header-error.immutable`.
+ ///
+ /// This headers resource is a child: it must be dropped before the parent
+ /// `outgoing-request` is dropped, or its ownership is transfered to
+ /// another component by e.g. `outgoing-handler.handle`.
+ headers: func() -> headers;
+
+ /// Returns the resource corresponding to the outgoing Body for this Response.
+ ///
+ /// Returns success on the first call: the `outgoing-body` resource for
+ /// this `outgoing-response` can be retrieved at most once. Subsequent
+ /// calls will return error.
+ body: func() -> result;
+ }
+
+ /// Represents an outgoing HTTP Request or Response's Body.
+ ///
+ /// A body has both its contents - a stream of bytes - and a (possibly
+ /// empty) set of trailers, inducating the full contents of the body
+ /// have been sent. This resource represents the contents as an
+ /// `output-stream` child resource, and the completion of the body (with
+ /// optional trailers) with a static function that consumes the
+ /// `outgoing-body` resource, and ensures that the user of this interface
+ /// may not write to the body contents after the body has been finished.
+ ///
+ /// If the user code drops this resource, as opposed to calling the static
+ /// method `finish`, the implementation should treat the body as incomplete,
+ /// and that an error has occured. The implementation should propogate this
+ /// error to the HTTP protocol by whatever means it has available,
+ /// including: corrupting the body on the wire, aborting the associated
+ /// Request, or sending a late status code for the Response.
+ resource outgoing-body {
+
+ /// Returns a stream for writing the body contents.
+ ///
+ /// The returned `output-stream` is a child resource: it must be dropped
+ /// before the parent `outgoing-body` resource is dropped (or finished),
+ /// otherwise the `outgoing-body` drop or `finish` will trap.
+ ///
+ /// Returns success on the first call: the `output-stream` resource for
+ /// this `outgoing-body` may be retrieved at most once. Subsequent calls
+ /// will return error.
+ write: func() -> result;
+
+ /// Finalize an outgoing body, optionally providing trailers. This must be
+ /// called to signal that the response is complete. If the `outgoing-body`
+ /// is dropped without calling `outgoing-body.finalize`, the implementation
+ /// should treat the body as corrupted.
+ ///
+ /// Fails if the body's `outgoing-request` or `outgoing-response` was
+ /// constructed with a Content-Length header, and the contents written
+ /// to the body (via `write`) does not match the value given in the
+ /// Content-Length.
+ finish: static func(
+ this: outgoing-body,
+ trailers: option
+ ) -> result<_, error-code>;
+ }
+
+ /// Represents a future which may eventaully return an incoming HTTP
+ /// Response, or an error.
+ ///
+ /// This resource is returned by the `wasi:http/outgoing-handler` interface to
+ /// provide the HTTP Response corresponding to the sent Request.
+ resource future-incoming-response {
+ /// Returns a pollable which becomes ready when either the Response has
+ /// been received, or an error has occured. When this pollable is ready,
+ /// the `get` method will return `some`.
+ subscribe: func() -> pollable;
+
+ /// Returns the incoming HTTP Response, or an error, once one is ready.
+ ///
+ /// The outer `option` represents future readiness. Users can wait on this
+ /// `option` to become `some` using the `subscribe` method.
+ ///
+ /// The outer `result` is used to retrieve the response or error at most
+ /// once. It will be success on the first call in which the outer option
+ /// is `some`, and error on subsequent calls.
+ ///
+ /// The inner `result` represents that either the incoming HTTP Response
+ /// status and headers have recieved successfully, or that an error
+ /// occured. Errors may also occur while consuming the response body,
+ /// but those will be reported by the `incoming-body` and its
+ /// `output-stream` child.
+ get: func() -> option>>;
+
+ }
+}