crossbind
GitHub

cURL for WASI

v8.22.0WASI

cURL 8.22.0 for command-line programs under wasmtime, precompiled for wasm32-wasip3, single-threaded as @crossbind/port-curl-wasi.

npm install @crossbind/port-curl-wasi@beta

Install

shell
npm install @crossbind/port-curl-wasi@beta crossbind@beta

A URL tool on libcurl, and the curl command itself

WASI has no JavaScript bindings: the program is a main() that links libcurl, built into one .wasm and run with wasmtime. This one is a small trurl: it parses a URL with the flags curl uses before a transfer, follows a Location header and appends encoded query pairs, without opening a connection. For transfers, @crossbind/port-curl-bin-wasi ships the curl command itself, which fetches over wasi:sockets with OpenSSL and verifies certificates: npx -p @crossbind/port-curl-bin-wasi@beta curl-wasi -sS https://example.com -o page.html.

crossbind.config.js
import curlWasi from '@crossbind/port-curl-wasi/crossbind.config.js';
 
export default {
general: { name: 'url-tool' },
dependencies: [curlWasi],
// A separate output folder: crossbind 2.0.0-beta.60 stops with ENOENT at the end of a WASI build
// whose dependencies ship data (OpenSSL's CA certificates here) when output is the build folder.
paths: { config: import.meta.url, output: 'dist' },
};
src/native/main.cpp
// A trurl-style URL tool on libcurl's URL API, for WASI. It parses a URL with the flags libcurl uses
// for CURLOPT_URL, then --follow resolves a Location header against it the way curl follows a
// redirect and --append adds a URL-encoded query pair. It prints the URL and its parts and opens no
// connection.
// url-tool <url> [--follow <location>] [--append <name=value>]...
#include <curl/curl.h>
 
#include <cstdio>
#include <cstring>
#include <string>
 
namespace {
 
constexpr unsigned int TRANSFER = CURLU_GUESS_SCHEME | CURLU_NON_SUPPORT_SCHEME;
constexpr unsigned int REDIRECT = CURLU_URLENCODE | CURLU_ALLOW_SPACE;
 
int usage() {
std::fprintf(stderr, "usage: url-tool <url> [--follow <location>] [--append <name=value>]...\n");
return 2;
}
 
// Adds "name=value" for a part the URL has, nothing for one it lacks.
void part(std::string& line, CURLU* url, const char* name, CURLUPart which, unsigned int flags) {
char* value = nullptr;
if (curl_url_get(url, which, &value, flags) != CURLUE_OK) return;
line += (line.empty() ? "" : " ") + std::string(name) + "=" + value;
curl_free(value);
}
 
} // namespace
 
int main(int argc, char** argv) {
if (argc < 2 || argc % 2 != 0) return usage();
for (int i = 2; i < argc; i += 2) {
if (std::strcmp(argv[i], "--follow") != 0 && std::strcmp(argv[i], "--append") != 0) return usage();
}
CURLU* url = curl_url();
if (!url) return 1;
CURLUcode code = curl_url_set(url, CURLUPART_URL, argv[1], TRANSFER);
for (int i = 2; code == CURLUE_OK && i < argc; i += 2) {
code = std::strcmp(argv[i], "--follow") == 0 ? curl_url_set(url, CURLUPART_URL, argv[i + 1], REDIRECT)
: curl_url_set(url, CURLUPART_QUERY, argv[i + 1], CURLU_APPENDQUERY | CURLU_URLENCODE);
}
char* whole = nullptr;
if (code == CURLUE_OK) code = curl_url_get(url, CURLUPART_URL, &whole, 0);
if (code != CURLUE_OK) {
std::fprintf(stderr, "url-tool: %s\n", curl_url_strerror(code));
curl_url_cleanup(url);
return 1;
}
std::printf("%s\n", whole);
curl_free(whole);
std::string parts;
part(parts, url, "scheme", CURLUPART_SCHEME, 0);
part(parts, url, "user", CURLUPART_USER, 0);
part(parts, url, "host", CURLUPART_HOST, 0);
part(parts, url, "port", CURLUPART_PORT, CURLU_DEFAULT_PORT);
part(parts, url, "path", CURLUPART_PATH, 0);
part(parts, url, "query", CURLUPART_QUERY, 0);
part(parts, url, "fragment", CURLUPART_FRAGMENT, 0);
std::printf("%s\n", parts.c_str());
curl_url_cleanup(url);
return 0;
}
shell
npx crossbind build -p wasi -b release
wasmtime run .crossbind/build/url-tool-wasi-wasm32-st-release.wasm 'http://example.com\@attacker.example/'
wasmtime run .crossbind/build/url-tool-wasi-wasm32-st-release.wasm https://example.com/a/b/c --follow '../d?y=2'
wasmtime run .crossbind/build/url-tool-wasi-wasm32-st-release.wasm https://example.com/search --append 'q=crème brûlée & tea' --append 'sort=price+asc'
output
http://example.com\@attacker.example/
scheme=http user=example.com\ host=attacker.example port=80 path=/
https://example.com/a/d?y=2
scheme=https host=example.com port=443 path=/a/d query=y=2
https://example.com/search?q=cr%C3%A8me+br%C3%BBl%C3%A9e+%26+tea&sort=price%2Basc
scheme=https host=example.com port=443 path=/search query=q=cr%C3%A8me+br%C3%BBl%C3%A9e+%26+tea&sort=price%2Basc

Command-line tools

One upstream command ships as npm executables built for wasm32-wasip3. They need wasmtime on PATH and no compiler - see WASI commands.

shell
npm install --global @crossbind/port-curl-bin-wasi@beta
curl-wasi --help
  • curl-wasi

What is different on WASI

  • There are no JavaScript bindings: src/native provides main(int, char**), and the build is a single .wasm.
  • Files come from the host through --dir preopens; --dir=. gives the program the current directory.
  • crossbind build -p wasi -b release writes the program to .crossbind/build/<name>-wasi-wasm32-st-release.wasm.
  • WASI 0.3's wasi:cli/exit carries success or failure only, so any non-zero return from main reaches the shell as exit code 1.
  • The build needs wasi-sdk 34 or newer (WASI_SDK_PATH) or the crossbind Docker image; running needs wasmtime 47 or newer. See WASI.

Other platforms

Facts on this page come from the port manifests in the repository and from what npm served on beta when the site was built. See the Libraries guide for the full consumer flow.

MORE LIBRARIES
ExpatGDALGEOSGeoTIFFiconvLERClibjpeg-turbolibTIFFOpenSSLPROJSpatiaLiteSQLiteWebPzlibZstandard
Type to search every guide page and section.
↑↓ navigate↵ openesc close